aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorNadir Chowdhury <[email protected]>2021-04-04 03:43:41 +0100
committerGitHub <[email protected]>2021-04-03 22:43:41 -0400
commit1b2688518eb5c229a7e8bee089d0c505cfae4018 (patch)
tree68a09f131d098b59698c9560dd94c8de54b9c2be
parent[docs] Add rtc_region parameter for Guild.create_voice_channel (diff)
downloaddiscord.py-1b2688518eb5c229a7e8bee089d0c505cfae4018.tar.xz
discord.py-1b2688518eb5c229a7e8bee089d0c505cfae4018.zip
Implement StageChannel and related methods
-rw-r--r--discord/channel.py280
-rw-r--r--discord/enums.py1
-rw-r--r--discord/ext/commands/converter.py41
-rw-r--r--discord/guild.py44
-rw-r--r--discord/http.py8
-rw-r--r--discord/member.py95
-rw-r--r--discord/permissions.py10
-rw-r--r--docs/api.rst15
8 files changed, 426 insertions, 68 deletions
diff --git a/discord/channel.py b/discord/channel.py
index 79441c96..28739edc 100644
--- a/discord/channel.py
+++ b/discord/channel.py
@@ -38,6 +38,7 @@ from .errors import ClientException, NoMoreItems, InvalidArgument
__all__ = (
'TextChannel',
'VoiceChannel',
+ 'StageChannel',
'DMChannel',
'CategoryChannel',
'StoreChannel',
@@ -537,51 +538,7 @@ class TextChannel(discord.abc.Messageable, discord.abc.GuildChannel, Hashable):
from .message import PartialMessage
return PartialMessage(channel=self, id=message_id)
-class VoiceChannel(discord.abc.Connectable, discord.abc.GuildChannel, Hashable):
- """Represents a Discord guild voice channel.
-
- .. container:: operations
-
- .. describe:: x == y
-
- Checks if two channels are equal.
-
- .. describe:: x != y
-
- Checks if two channels are not equal.
-
- .. describe:: hash(x)
-
- Returns the channel's hash.
-
- .. describe:: str(x)
-
- Returns the channel's name.
-
- Attributes
- -----------
- name: :class:`str`
- The channel name.
- guild: :class:`Guild`
- The guild the channel belongs to.
- id: :class:`int`
- The channel ID.
- category_id: Optional[:class:`int`]
- The category channel ID this channel belongs to, if applicable.
- position: :class:`int`
- The position in the channel list. This is a number that starts at 0. e.g. the
- top channel is position 0.
- bitrate: :class:`int`
- 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.
- A value of ``None`` indicates automatic voice region detection.
-
- .. versionadded:: 1.7
- """
-
+class VocalGuildChannel(discord.abc.Connectable, discord.abc.GuildChannel, Hashable):
__slots__ = ('name', 'id', 'guild', 'bitrate', 'user_limit',
'_state', 'position', '_overwrites', 'category_id',
'rtc_region')
@@ -591,29 +548,12 @@ class VoiceChannel(discord.abc.Connectable, discord.abc.GuildChannel, Hashable):
self.id = int(data['id'])
self._update(guild, data)
- def __repr__(self):
- attrs = [
- ('id', self.id),
- ('name', self.name),
- ('rtc_region', self.rtc_region),
- ('position', self.position),
- ('bitrate', self.bitrate),
- ('user_limit', self.user_limit),
- ('category_id', self.category_id)
- ]
- return '<%s %s>' % (self.__class__.__name__, ' '.join('%s=%r' % t for t in attrs))
-
def _get_voice_client_key(self):
return self.guild.id, 'guild_id'
def _get_voice_state_pair(self):
return self.guild.id, self.id
- @property
- def type(self):
- """:class:`ChannelType`: The channel's Discord type."""
- return ChannelType.voice
-
def _update(self, guild, data):
self.guild = guild
self.name = data['name']
@@ -671,6 +611,70 @@ class VoiceChannel(discord.abc.Connectable, discord.abc.GuildChannel, Hashable):
base.value &= ~denied.value
return base
+class VoiceChannel(VocalGuildChannel):
+ """Represents a Discord guild voice channel.
+
+ .. container:: operations
+
+ .. describe:: x == y
+
+ Checks if two channels are equal.
+
+ .. describe:: x != y
+
+ Checks if two channels are not equal.
+
+ .. describe:: hash(x)
+
+ Returns the channel's hash.
+
+ .. describe:: str(x)
+
+ Returns the channel's name.
+
+ Attributes
+ -----------
+ name: :class:`str`
+ The channel name.
+ guild: :class:`Guild`
+ The guild the channel belongs to.
+ id: :class:`int`
+ The channel ID.
+ category_id: Optional[:class:`int`]
+ The category channel ID this channel belongs to, if applicable.
+ position: :class:`int`
+ The position in the channel list. This is a number that starts at 0. e.g. the
+ top channel is position 0.
+ bitrate: :class:`int`
+ 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.
+ A value of ``None`` indicates automatic voice region detection.
+
+ .. versionadded:: 1.7
+ """
+
+ __slots__ = ()
+
+ def __repr__(self):
+ attrs = [
+ ('id', self.id),
+ ('name', self.name),
+ ('rtc_region', self.rtc_region),
+ ('position', self.position),
+ ('bitrate', self.bitrate),
+ ('user_limit', self.user_limit),
+ ('category_id', self.category_id)
+ ]
+ return '<%s %s>' % (self.__class__.__name__, ' '.join('%s=%r' % t for t in attrs))
+
+ @property
+ def type(self):
+ """:class:`ChannelType`: The channel's Discord type."""
+ return ChannelType.voice
+
@utils.copy_doc(discord.abc.GuildChannel.clone)
async def clone(self, *, name=None, reason=None):
return await self._clone_impl({
@@ -728,6 +732,130 @@ class VoiceChannel(discord.abc.Connectable, discord.abc.GuildChannel, Hashable):
await self._edit(options, reason=reason)
+class StageChannel(VocalGuildChannel):
+ """Represents a Discord guild stage channel.
+
+ .. versionadded:: 1.7
+
+ .. container:: operations
+
+ .. describe:: x == y
+
+ Checks if two channels are equal.
+
+ .. describe:: x != y
+
+ Checks if two channels are not equal.
+
+ .. describe:: hash(x)
+
+ Returns the channel's hash.
+
+ .. describe:: str(x)
+
+ Returns the channel's name.
+
+ Attributes
+ -----------
+ name: :class:`str`
+ The channel name.
+ guild: :class:`Guild`
+ The guild the channel belongs to.
+ id: :class:`int`
+ The channel ID.
+ topic: Optional[:class:`str`]
+ The channel's topic. ``None`` if it isn't set.
+ category_id: Optional[:class:`int`]
+ The category channel ID this channel belongs to, if applicable.
+ position: :class:`int`
+ The position in the channel list. This is a number that starts at 0. e.g. the
+ top channel is position 0.
+ bitrate: :class:`int`
+ 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 stage channel.
+ rtc_region: Optional[:class:`VoiceRegion`]
+ The region for the stage channel's voice communication.
+ A value of ``None`` indicates automatic voice region detection.
+ """
+ __slots__ = ('topic',)
+
+ def __repr__(self):
+ attrs = [
+ ('id', self.id),
+ ('name', self.name),
+ ('topic', self.topic),
+ ('rtc_region', self.rtc_region),
+ ('position', self.position),
+ ('bitrate', self.bitrate),
+ ('user_limit', self.user_limit),
+ ('category_id', self.category_id)
+ ]
+ return '<%s %s>' % (self.__class__.__name__, ' '.join('%s=%r' % t for t in attrs))
+
+ def _update(self, guild, data):
+ super()._update(guild, data)
+ self.topic = data.get('topic')
+
+ @property
+ def requesting_to_speak(self):
+ """List[:class:`Member`]: A list of members who are requesting to speak in the stage channel."""
+ return [member for member in self.members if member.voice.requested_to_speak_at is not None]
+
+ @property
+ def type(self):
+ """:class:`ChannelType`: The channel's Discord type."""
+ return ChannelType.stage_voice
+
+ @utils.copy_doc(discord.abc.GuildChannel.clone)
+ async def clone(self, *, name=None, reason=None):
+ return await self._clone_impl({
+ 'topic': self.topic,
+ }, name=name, reason=reason)
+
+ async def edit(self, *, reason=None, **options):
+ """|coro|
+
+ Edits the channel.
+
+ You must have the :attr:`~Permissions.manage_channels` permission to
+ use this.
+
+ Parameters
+ ----------
+ name: :class:`str`
+ The new channel's name.
+ topic: :class:`str`
+ The new channel's topic.
+ position: :class:`int`
+ The new channel's position.
+ sync_permissions: :class:`bool`
+ Whether to sync permissions with the channel's new or pre-existing
+ category. Defaults to ``False``.
+ category: Optional[:class:`CategoryChannel`]
+ The new category for this channel. Can be ``None`` to remove the
+ category.
+ reason: Optional[:class:`str`]
+ The reason for editing this channel. Shows up on the audit log.
+ overwrites: :class:`dict`
+ A :class:`dict` of target (either a role or a member) to
+ :class:`PermissionOverwrite` to apply to the channel.
+ rtc_region: Optional[:class:`VoiceRegion`]
+ The new region for the stage channel's voice communication.
+ A value of ``None`` indicates automatic voice region detection.
+
+ Raises
+ ------
+ InvalidArgument
+ If the permission overwrite information is not in proper form.
+ Forbidden
+ You do not have permissions to edit the channel.
+ HTTPException
+ Editing the channel failed.
+ """
+
+ await self._edit(options, reason=reason)
+
class CategoryChannel(discord.abc.GuildChannel, Hashable):
"""Represents a Discord channel category.
@@ -874,6 +1002,18 @@ class CategoryChannel(discord.abc.GuildChannel, Hashable):
ret.sort(key=lambda c: (c.position, c.id))
return ret
+ @property
+ def stage_channels(self):
+ """List[:class:`StageChannel`]: Returns the voice channels that are under this category.
+
+ .. versionadded:: 1.7
+ """
+ ret = [c for c in self.guild.channels
+ if c.category_id == self.id
+ and isinstance(c, StageChannel)]
+ ret.sort(key=lambda c: (c.position, c.id))
+ return ret
+
async def create_text_channel(self, name, *, overwrites=None, reason=None, **options):
"""|coro|
@@ -898,6 +1038,20 @@ class CategoryChannel(discord.abc.GuildChannel, Hashable):
"""
return await self.guild.create_voice_channel(name, overwrites=overwrites, category=self, reason=reason, **options)
+ async def create_stage_channel(self, name, *, overwrites=None, reason=None, **options):
+ """|coro|
+
+ A shortcut method to :meth:`Guild.create_stage_channel` to create a :class:`StageChannel` in the category.
+
+ .. versionadded:: 1.7
+
+ Returns
+ -------
+ :class:`StageChannel`
+ The channel that was just created.
+ """
+ return await self.guild.create_stage_channel(name, overwrites=overwrites, category=self, reason=reason, **options)
+
class StoreChannel(discord.abc.GuildChannel, Hashable):
"""Represents a Discord guild store channel.
@@ -1407,5 +1561,7 @@ def _channel_factory(channel_type):
return TextChannel, value
elif value is ChannelType.store:
return StoreChannel, value
+ elif value is ChannelType.stage_voice:
+ return StageChannel, value
else:
return None, value
diff --git a/discord/enums.py b/discord/enums.py
index 93194788..38ff285f 100644
--- a/discord/enums.py
+++ b/discord/enums.py
@@ -158,6 +158,7 @@ class ChannelType(Enum):
category = 4
news = 5
store = 6
+ stage_voice = 13
def __str__(self):
return self.name
diff --git a/discord/ext/commands/converter.py b/discord/ext/commands/converter.py
index 2da0e110..afbea4f2 100644
--- a/discord/ext/commands/converter.py
+++ b/discord/ext/commands/converter.py
@@ -46,6 +46,7 @@ __all__ = (
'ColourConverter',
'ColorConverter',
'VoiceChannelConverter',
+ 'StageChannelConverter',
'EmojiConverter',
'PartialEmojiConverter',
'CategoryChannelConverter',
@@ -396,6 +397,46 @@ class VoiceChannelConverter(IDConverter):
return result
+class StageChannelConverter(IDConverter):
+ """Converts to a :class:`~discord.StageChannel`.
+
+ .. versionadded:: 1.7
+
+ All lookups are via the local guild. If in a DM context, then the lookup
+ is done by the global cache.
+
+ The lookup strategy is as follows (in order):
+
+ 1. Lookup by ID.
+ 2. Lookup by mention.
+ 3. Lookup by name
+ """
+ async def convert(self, ctx, argument):
+ bot = ctx.bot
+ match = self._get_id_match(argument) or re.match(r'<#([0-9]+)>$', argument)
+ result = None
+ guild = ctx.guild
+
+ if match is None:
+ # not a mention
+ if guild:
+ result = discord.utils.get(guild.stage_channels, name=argument)
+ else:
+ def check(c):
+ return isinstance(c, discord.StageChannel) and c.name == argument
+ result = discord.utils.find(check, bot.get_all_channels())
+ else:
+ channel_id = int(match.group(1))
+ if guild:
+ result = guild.get_channel(channel_id)
+ else:
+ result = _get_from_guilds(bot, 'get_channel', channel_id)
+
+ if not isinstance(result, discord.StageChannel):
+ raise ChannelNotFound(argument)
+
+ return result
+
class CategoryChannelConverter(IDConverter):
"""Converts to a :class:`~discord.CategoryChannel`.
diff --git a/discord/guild.py b/discord/guild.py
index 4b7ade89..2cc5679f 100644
--- a/discord/guild.py
+++ b/discord/guild.py
@@ -372,6 +372,18 @@ class Guild(Hashable):
return r
@property
+ def stage_channels(self):
+ """List[:class:`StageChannel`]: A list of voice channels that belongs to this guild.
+
+ .. versionadded:: 1.7
+
+ This is sorted by the position and are in UI order from top to bottom.
+ """
+ r = [ch for ch in self._channels.values() if isinstance(ch, StageChannel)]
+ r.sort(key=lambda c: (c.position, c.id))
+ return r
+
+ @property
def me(self):
""":class:`Member`: Similar to :attr:`Client.user` except an instance of :class:`Member`.
This is essentially used to get the member version of yourself.
@@ -979,6 +991,38 @@ class Guild(Hashable):
self._channels[channel.id] = channel
return channel
+ async def create_stage_channel(self, name, *, topic=None, category=None, overwrites=None, reason=None, position=None):
+ """|coro|
+
+ This is similar to :meth:`create_text_channel` except makes a :class:`StageChannel` instead.
+
+ .. note::
+
+ The ``slowmode_delay`` and ``nsfw`` parameters are not supported in this function.
+
+ .. versionadded:: 1.7
+
+ Raises
+ ------
+ Forbidden
+ You do not have the proper permissions to create this channel.
+ HTTPException
+ Creating the channel failed.
+ InvalidArgument
+ The permission overwrite information is not in proper form.
+
+ Returns
+ -------
+ :class:`StageChannel`
+ The channel that was just created.
+ """
+ data = await self._create_channel(name, overwrites, ChannelType.stage_voice, category, reason=reason, position=position, topic=topic)
+ channel = StageChannel(state=self._state, guild=self, data=data)
+
+ # temporarily add to the cache
+ self._channels[channel.id] = channel
+ return channel
+
async def create_category(self, name, *, overwrites=None, reason=None, position=None):
"""|coro|
diff --git a/discord/http.py b/discord/http.py
index 9a95c65a..3f63a75d 100644
--- a/discord/http.py
+++ b/discord/http.py
@@ -574,6 +574,14 @@ class HTTPClient:
}
return self.request(r, json=payload, reason=reason)
+ def edit_my_voice_state(self, guild_id, payload):
+ r = Route('PATCH', '/guilds/{guild_id}/voice-states/@me', guild_id=guild_id)
+ return self.request(r, json=payload)
+
+ def edit_voice_state(self, guild_id, user_id, payload):
+ r = Route('PATCH', '/guilds/{guild_id}/voice-states/{user_id}', guild_id=guild_id, user_id=user_id)
+ return self.request(r, json=payload)
+
def edit_member(self, guild_id, user_id, *, reason=None, **fields):
r = Route('PATCH', '/guilds/{guild_id}/members/{user_id}', guild_id=guild_id, user_id=user_id)
return self.request(r, json=fields, reason=reason)
diff --git a/discord/member.py b/discord/member.py
index a72e9945..d0e969bd 100644
--- a/discord/member.py
+++ b/discord/member.py
@@ -24,6 +24,7 @@ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
"""
+import datetime
import inspect
import itertools
import sys
@@ -32,6 +33,7 @@ from operator import attrgetter
import discord.abc
from . import utils
+from .errors import ClientException
from .user import BaseUser, User
from .activity import create_activity
from .permissions import Permissions
@@ -59,15 +61,32 @@ class VoiceState:
self_video: :class:`bool`
Indicates if the user is currently broadcasting video.
+ suppress: :class:`bool`
+ Indicates if the user is suppressed from speaking.
+
+ Only applies to stage channels.
+
+ .. versionadded:: 1.7
+
+ requested_to_speak_at: Optional[:class:`datetime.datetime`]
+ A datetime object that specifies the date and time in UTC that the member
+ requested to speak. It will be ``None`` if they are not requesting to speak
+ anymore or have been accepted to speak.
+
+ Only applicable to stage channels.
+
+ .. versionadded:: 1.7
+
afk: :class:`bool`
Indicates if the user is currently in the AFK channel in the guild.
- channel: Optional[:class:`VoiceChannel`]
+ channel: Optional[Union[:class:`VoiceChannel`, :class:`StageChannel`]]
The voice channel that the user is currently connected to. ``None`` if the user
is not currently in a voice channel.
"""
__slots__ = ('session_id', 'deaf', 'mute', 'self_mute',
- 'self_stream', 'self_video', 'self_deaf', 'afk', 'channel')
+ 'self_stream', 'self_video', 'self_deaf', 'afk', 'channel',
+ 'requested_to_speak_at', 'suppress')
def __init__(self, *, data, channel=None):
self.session_id = data.get('session_id')
@@ -81,10 +100,20 @@ class VoiceState:
self.afk = data.get('suppress', False)
self.mute = data.get('mute', False)
self.deaf = data.get('deaf', False)
+ self.suppress = data.get('suppress', False)
+ self.requested_to_speak_at = utils.parse_time(data.get('request_to_speak_timestamp'))
self.channel = channel
def __repr__(self):
- return '<VoiceState self_mute={0.self_mute} self_deaf={0.self_deaf} self_stream={0.self_stream} channel={0.channel!r}>'.format(self)
+ attrs = [
+ ('self_mute', self.self_mute),
+ ('self_deaf', self.self_deaf),
+ ('self_stream', self.self_stream),
+ ('suppress', self.suppress),
+ ('requested_to_speak_at', self.requested_to_speak_at),
+ ('channel', self.channel)
+ ]
+ return '<%s %s>' % (self.__class__.__name__, ' '.join('%s=%r' % t for t in attrs))
def flatten_user(cls):
for attr, value in itertools.chain(BaseUser.__dict__.items(), User.__dict__.items()):
@@ -559,6 +588,11 @@ class Member(discord.abc.Messageable, _BaseUser):
Indicates if the member should be guild muted or un-muted.
deafen: :class:`bool`
Indicates if the member should be guild deafened or un-deafened.
+ suppress: :class:`bool`
+ Indicates if the member should be suppressed in stage channels.
+
+ .. versionadded:: 1.7
+
roles: Optional[List[:class:`Role`]]
The member's new list of roles. This *replaces* the roles.
voice_channel: Optional[:class:`VoiceChannel`]
@@ -576,6 +610,7 @@ class Member(discord.abc.Messageable, _BaseUser):
"""
http = self._state.http
guild_id = self.guild.id
+ me = self._state.self_id == self.id
payload = {}
try:
@@ -585,7 +620,7 @@ class Member(discord.abc.Messageable, _BaseUser):
pass
else:
nick = nick or ''
- if self._state.self_id == self.id:
+ if me:
await http.change_my_nickname(guild_id, nick, reason=reason)
else:
payload['nick'] = nick
@@ -598,6 +633,23 @@ class Member(discord.abc.Messageable, _BaseUser):
if mute is not None:
payload['mute'] = mute
+ suppress = fields.get('suppress')
+ if suppress is not None:
+ voice_state_payload = {
+ 'channel_id': self.voice.channel.id,
+ 'suppress': suppress,
+ }
+
+ if suppress or self.bot:
+ voice_state_payload['request_to_speak_timestamp'] = None
+
+ if me:
+ await http.edit_my_voice_state(guild_id, voice_state_payload)
+ else:
+ if not suppress:
+ voice_state_payload['request_to_speak_timestamp'] = datetime.datetime.utcnow().isoformat()
+ await http.edit_voice_state(guild_id, self.id, voice_state_payload)
+
try:
vc = fields['voice_channel']
except KeyError:
@@ -612,10 +664,43 @@ class Member(discord.abc.Messageable, _BaseUser):
else:
payload['roles'] = tuple(r.id for r in roles)
- await http.edit_member(guild_id, self.id, reason=reason, **payload)
+ if payload:
+ await http.edit_member(guild_id, self.id, reason=reason, **payload)
# TODO: wait for WS event for modify-in-place behaviour
+ async def request_to_speak(self):
+ """|coro|
+
+ Request to speak in the connected channel.
+
+ Only applies to stage channels.
+
+ .. note::
+
+ Requesting members that are not the client is equivalent
+ to :attr:`.edit` providing ``suppress`` as ``False``.
+
+ .. versionadded:: 1.7
+
+ Raises
+ -------
+ Forbidden
+ You do not have the proper permissions to the action requested.
+ HTTPException
+ The operation failed.
+ """
+ payload = {
+ 'channel_id': self.voice.channel.id,
+ 'request_to_speak_timestamp': datetime.datetime.utcnow().isoformat(),
+ }
+
+ if self._state.self_id != self.id:
+ payload['suppress'] = False
+ await self._state.http.edit_voice_state(self.guild.id, self.id, payload)
+ else:
+ await self._state.http.edit_my_voice_state(self.guild.id, payload)
+
async def move_to(self, channel, *, reason=None):
"""|coro|
diff --git a/discord/permissions.py b/discord/permissions.py
index 1b0dd5a2..3fd0dacf 100644
--- a/discord/permissions.py
+++ b/discord/permissions.py
@@ -214,6 +214,15 @@ class Permissions(BaseFlags):
return cls(1 << 32)
@classmethod
+ def stage_moderator(cls):
+ """A factory method that creates a :class:`Permissions` with all
+ "Stage Moderator" permissions from the official Discord UI set to ``True``.
+
+ .. versionadded:: 1.7
+ """
+ return cls(0b100000001010000000000000000000000)
+
+ @classmethod
def advanced(cls):
"""A factory method that creates a :class:`Permissions` with all
"Advanced" permissions from the official Discord UI set to ``True``.
@@ -222,7 +231,6 @@ class Permissions(BaseFlags):
"""
return cls(1 << 3)
-
def update(self, **kwargs):
r"""Bulk updates this permission object.
diff --git a/docs/api.rst b/docs/api.rst
index 844f1807..3ed59c04 100644
--- a/docs/api.rst
+++ b/docs/api.rst
@@ -1074,6 +1074,12 @@ of :class:`enum.Enum`.
A guild store channel.
+ .. attribute:: stage_voice
+
+ A guild stage voice channel.
+
+ .. versionadded:: 1.7
+
.. class:: MessageType
Specifies the type of :class:`Message`. This is used to denote if a message
@@ -3038,6 +3044,15 @@ VoiceChannel
:members:
:inherited-members:
+StageChannel
+~~~~~~~~~~~~~
+
+.. attributetable:: StageChannel
+
+.. autoclass:: StageChannel()
+ :members:
+ :inherited-members:
+
CategoryChannel
~~~~~~~~~~~~~~~~~