Browse Source

Implement StageChannel and related methods

pull/6818/head
Nadir Chowdhury 4 years ago
committed by GitHub
parent
commit
1b2688518e
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 280
      discord/channel.py
  2. 1
      discord/enums.py
  3. 41
      discord/ext/commands/converter.py
  4. 44
      discord/guild.py
  5. 8
      discord/http.py
  6. 95
      discord/member.py
  7. 10
      discord/permissions.py
  8. 15
      docs/api.rst

280
discord/channel.py

@ -38,6 +38,7 @@ from .errors import ClientException, NoMoreItems, InvalidArgument
__all__ = ( __all__ = (
'TextChannel', 'TextChannel',
'VoiceChannel', 'VoiceChannel',
'StageChannel',
'DMChannel', 'DMChannel',
'CategoryChannel', 'CategoryChannel',
'StoreChannel', 'StoreChannel',
@ -537,51 +538,7 @@ class TextChannel(discord.abc.Messageable, discord.abc.GuildChannel, Hashable):
from .message import PartialMessage from .message import PartialMessage
return PartialMessage(channel=self, id=message_id) return PartialMessage(channel=self, id=message_id)
class VoiceChannel(discord.abc.Connectable, discord.abc.GuildChannel, Hashable): class VocalGuildChannel(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
"""
__slots__ = ('name', 'id', 'guild', 'bitrate', 'user_limit', __slots__ = ('name', 'id', 'guild', 'bitrate', 'user_limit',
'_state', 'position', '_overwrites', 'category_id', '_state', 'position', '_overwrites', 'category_id',
'rtc_region') 'rtc_region')
@ -591,29 +548,12 @@ class VoiceChannel(discord.abc.Connectable, discord.abc.GuildChannel, Hashable):
self.id = int(data['id']) self.id = int(data['id'])
self._update(guild, data) 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): def _get_voice_client_key(self):
return self.guild.id, 'guild_id' return self.guild.id, 'guild_id'
def _get_voice_state_pair(self): def _get_voice_state_pair(self):
return self.guild.id, self.id 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): def _update(self, guild, data):
self.guild = guild self.guild = guild
self.name = data['name'] self.name = data['name']
@ -671,6 +611,70 @@ class VoiceChannel(discord.abc.Connectable, discord.abc.GuildChannel, Hashable):
base.value &= ~denied.value base.value &= ~denied.value
return base 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) @utils.copy_doc(discord.abc.GuildChannel.clone)
async def clone(self, *, name=None, reason=None): async def clone(self, *, name=None, reason=None):
return await self._clone_impl({ return await self._clone_impl({
@ -728,6 +732,130 @@ class VoiceChannel(discord.abc.Connectable, discord.abc.GuildChannel, Hashable):
await self._edit(options, reason=reason) 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): class CategoryChannel(discord.abc.GuildChannel, Hashable):
"""Represents a Discord channel category. """Represents a Discord channel category.
@ -874,6 +1002,18 @@ class CategoryChannel(discord.abc.GuildChannel, Hashable):
ret.sort(key=lambda c: (c.position, c.id)) ret.sort(key=lambda c: (c.position, c.id))
return ret 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): async def create_text_channel(self, name, *, overwrites=None, reason=None, **options):
"""|coro| """|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) 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): class StoreChannel(discord.abc.GuildChannel, Hashable):
"""Represents a Discord guild store channel. """Represents a Discord guild store channel.
@ -1407,5 +1561,7 @@ def _channel_factory(channel_type):
return TextChannel, value return TextChannel, value
elif value is ChannelType.store: elif value is ChannelType.store:
return StoreChannel, value return StoreChannel, value
elif value is ChannelType.stage_voice:
return StageChannel, value
else: else:
return None, value return None, value

1
discord/enums.py

@ -158,6 +158,7 @@ class ChannelType(Enum):
category = 4 category = 4
news = 5 news = 5
store = 6 store = 6
stage_voice = 13
def __str__(self): def __str__(self):
return self.name return self.name

41
discord/ext/commands/converter.py

@ -46,6 +46,7 @@ __all__ = (
'ColourConverter', 'ColourConverter',
'ColorConverter', 'ColorConverter',
'VoiceChannelConverter', 'VoiceChannelConverter',
'StageChannelConverter',
'EmojiConverter', 'EmojiConverter',
'PartialEmojiConverter', 'PartialEmojiConverter',
'CategoryChannelConverter', 'CategoryChannelConverter',
@ -396,6 +397,46 @@ class VoiceChannelConverter(IDConverter):
return result 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): class CategoryChannelConverter(IDConverter):
"""Converts to a :class:`~discord.CategoryChannel`. """Converts to a :class:`~discord.CategoryChannel`.

44
discord/guild.py

@ -371,6 +371,18 @@ class Guild(Hashable):
r.sort(key=lambda c: (c.position, c.id)) r.sort(key=lambda c: (c.position, c.id))
return r 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 @property
def me(self): def me(self):
""":class:`Member`: Similar to :attr:`Client.user` except an instance of :class:`Member`. """:class:`Member`: Similar to :attr:`Client.user` except an instance of :class:`Member`.
@ -979,6 +991,38 @@ class Guild(Hashable):
self._channels[channel.id] = channel self._channels[channel.id] = channel
return 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): async def create_category(self, name, *, overwrites=None, reason=None, position=None):
"""|coro| """|coro|

8
discord/http.py

@ -574,6 +574,14 @@ class HTTPClient:
} }
return self.request(r, json=payload, reason=reason) 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): 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) 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) return self.request(r, json=fields, reason=reason)

95
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. DEALINGS IN THE SOFTWARE.
""" """
import datetime
import inspect import inspect
import itertools import itertools
import sys import sys
@ -32,6 +33,7 @@ from operator import attrgetter
import discord.abc import discord.abc
from . import utils from . import utils
from .errors import ClientException
from .user import BaseUser, User from .user import BaseUser, User
from .activity import create_activity from .activity import create_activity
from .permissions import Permissions from .permissions import Permissions
@ -59,15 +61,32 @@ class VoiceState:
self_video: :class:`bool` self_video: :class:`bool`
Indicates if the user is currently broadcasting video. 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` afk: :class:`bool`
Indicates if the user is currently in the AFK channel in the guild. 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 The voice channel that the user is currently connected to. ``None`` if the user
is not currently in a voice channel. is not currently in a voice channel.
""" """
__slots__ = ('session_id', 'deaf', 'mute', 'self_mute', __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): def __init__(self, *, data, channel=None):
self.session_id = data.get('session_id') self.session_id = data.get('session_id')
@ -81,10 +100,20 @@ class VoiceState:
self.afk = data.get('suppress', False) self.afk = data.get('suppress', False)
self.mute = data.get('mute', False) self.mute = data.get('mute', False)
self.deaf = data.get('deaf', 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 self.channel = channel
def __repr__(self): 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): def flatten_user(cls):
for attr, value in itertools.chain(BaseUser.__dict__.items(), User.__dict__.items()): 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. Indicates if the member should be guild muted or un-muted.
deafen: :class:`bool` deafen: :class:`bool`
Indicates if the member should be guild deafened or un-deafened. 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`]] roles: Optional[List[:class:`Role`]]
The member's new list of roles. This *replaces* the roles. The member's new list of roles. This *replaces* the roles.
voice_channel: Optional[:class:`VoiceChannel`] voice_channel: Optional[:class:`VoiceChannel`]
@ -576,6 +610,7 @@ class Member(discord.abc.Messageable, _BaseUser):
""" """
http = self._state.http http = self._state.http
guild_id = self.guild.id guild_id = self.guild.id
me = self._state.self_id == self.id
payload = {} payload = {}
try: try:
@ -585,7 +620,7 @@ class Member(discord.abc.Messageable, _BaseUser):
pass pass
else: else:
nick = nick or '' nick = nick or ''
if self._state.self_id == self.id: if me:
await http.change_my_nickname(guild_id, nick, reason=reason) await http.change_my_nickname(guild_id, nick, reason=reason)
else: else:
payload['nick'] = nick payload['nick'] = nick
@ -598,6 +633,23 @@ class Member(discord.abc.Messageable, _BaseUser):
if mute is not None: if mute is not None:
payload['mute'] = mute 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: try:
vc = fields['voice_channel'] vc = fields['voice_channel']
except KeyError: except KeyError:
@ -612,10 +664,43 @@ class Member(discord.abc.Messageable, _BaseUser):
else: else:
payload['roles'] = tuple(r.id for r in roles) 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 # 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): async def move_to(self, channel, *, reason=None):
"""|coro| """|coro|

10
discord/permissions.py

@ -213,6 +213,15 @@ class Permissions(BaseFlags):
""" """
return cls(1 << 32) 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 @classmethod
def advanced(cls): def advanced(cls):
"""A factory method that creates a :class:`Permissions` with all """A factory method that creates a :class:`Permissions` with all
@ -222,7 +231,6 @@ class Permissions(BaseFlags):
""" """
return cls(1 << 3) return cls(1 << 3)
def update(self, **kwargs): def update(self, **kwargs):
r"""Bulk updates this permission object. r"""Bulk updates this permission object.

15
docs/api.rst

@ -1074,6 +1074,12 @@ of :class:`enum.Enum`.
A guild store channel. A guild store channel.
.. attribute:: stage_voice
A guild stage voice channel.
.. versionadded:: 1.7
.. class:: MessageType .. class:: MessageType
Specifies the type of :class:`Message`. This is used to denote if a message Specifies the type of :class:`Message`. This is used to denote if a message
@ -3038,6 +3044,15 @@ VoiceChannel
:members: :members:
:inherited-members: :inherited-members:
StageChannel
~~~~~~~~~~~~~
.. attributetable:: StageChannel
.. autoclass:: StageChannel()
:members:
:inherited-members:
CategoryChannel CategoryChannel
~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~

Loading…
Cancel
Save