Browse Source

Implement StageInstance

pull/7005/head
Nadir Chowdhury 4 years ago
committed by GitHub
parent
commit
9f98a9a87f
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      discord/__init__.py
  2. 123
      discord/channel.py
  3. 55
      discord/client.py
  4. 6
      discord/enums.py
  5. 36
      discord/guild.py
  6. 29
      discord/http.py
  7. 168
      discord/stage_instance.py
  8. 35
      discord/state.py
  9. 12
      discord/types/channel.py
  10. 52
      docs/api.rst

1
discord/__init__.py

@ -55,6 +55,7 @@ from .audit_logs import *
from .raw_models import *
from .team import *
from .sticker import *
from .stage_instance import *
from .interactions import *
from .components import *

123
discord/channel.py

@ -30,11 +30,12 @@ from typing import Callable, Dict, List, Optional, TYPE_CHECKING, Union, overloa
import discord.abc
from .permissions import PermissionOverwrite, Permissions
from .enums import ChannelType, try_enum, VoiceRegion, VideoQualityMode
from .enums import ChannelType, StagePrivacyLevel, try_enum, VoiceRegion, VideoQualityMode
from .mixins import Hashable
from . import utils
from .asset import Asset
from .errors import ClientException, NoMoreItems, InvalidArgument
from .stage_instance import StageInstance
__all__ = (
'TextChannel',
@ -49,7 +50,7 @@ __all__ = (
if TYPE_CHECKING:
from .role import Role
from .member import Member
from .member import Member, VoiceState
from .abc import Snowflake
from .message import Message
from .webhook import Webhook
@ -611,7 +612,7 @@ class VocalGuildChannel(discord.abc.Connectable, discord.abc.GuildChannel, Hasha
return ChannelType.voice.value
@property
def members(self):
def members(self) -> List[Member]:
"""List[:class:`Member`]: Returns all members that are currently inside this voice channel."""
ret = []
for user_id, state in self.guild._voice_states.items():
@ -622,7 +623,7 @@ class VocalGuildChannel(discord.abc.Connectable, discord.abc.GuildChannel, Hasha
return ret
@property
def voice_states(self):
def voice_states(self) -> Dict[int, VoiceState]:
"""Returns a mapping of member IDs who have voice states in this channel.
.. versionadded:: 1.3
@ -640,7 +641,7 @@ class VocalGuildChannel(discord.abc.Connectable, discord.abc.GuildChannel, Hasha
return {key: value for key, value in self.guild._voice_states.items() if value.channel.id == self.id}
@utils.copy_doc(discord.abc.GuildChannel.permissions_for)
def permissions_for(self, member):
def permissions_for(self, member: Union[Role, Member], /) -> Permissions:
base = super().permissions_for(member)
# voice channels cannot be edited by people who can't connect to them
@ -875,10 +876,35 @@ class StageChannel(VocalGuildChannel):
self.topic = data.get('topic')
@property
def requesting_to_speak(self):
def requesting_to_speak(self) -> List[Member]:
"""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 speakers(self) -> List[Member]:
"""List[:class:`Member`]: A list of members who have been permitted to speak in the stage channel.
.. versionadded:: 2.0
"""
return [member for member in self.members if not member.voice.suppress and member.voice.requested_to_speak_at is None]
@property
def listeners(self) -> List[Member]:
"""List[:class:`Member`]: A list of members who are listening in the stage channel.
.. versionadded:: 2.0
"""
return [member for member in self.members if member.voice.suppress]
@property
def moderators(self) -> List[Member]:
"""List[:class:`Member`]: A list of members who are moderating the stage channel.
.. versionadded:: 2.0
"""
required_permissions = Permissions.stage_moderator()
return [member for member in self.members if self.permissions_for(member) >= required_permissions]
@property
def type(self):
""":class:`ChannelType`: The channel's Discord type."""
@ -886,9 +912,83 @@ class StageChannel(VocalGuildChannel):
@utils.copy_doc(discord.abc.GuildChannel.clone)
async def clone(self, *, name: str = None, reason: Optional[str] = None) -> StageChannel:
return await self._clone_impl({
'topic': self.topic,
}, name=name, reason=reason)
return await self._clone_impl({}, name=name, reason=reason)
@property
def instance(self) -> Optional[StageInstance]:
"""Optional[:class:`StageInstance`]: The running stage instance of the stage channel.
.. versionadded:: 2.0
"""
return utils.get(self.guild.stage_instances, channel_id=self.id)
async def create_instance(self, *, topic: str, privacy_level: StagePrivacyLevel = utils.MISSING) -> StageInstance:
"""|coro|
Create a stage instance.
You must have the :attr:`~Permissions.manage_channels` permission to
use this.
.. versionadded:: 2.0
Parameters
-----------
topic: :class:`str`
The stage instance's topic.
privacy_level: :class:`StagePrivacyLevel`
The stage instance's privacy level. Defaults to :attr:`PrivacyLevel.guild_only`.
Raises
------
InvalidArgument
If the ``privacy_level`` parameter is not the proper type.
Forbidden
You do not have permissions to create a stage instance.
HTTPException
Creating a stage instance failed.
Returns
--------
:class:`StageInstance`
The newly created stage instance.
"""
payload = {
'channel_id': self.id,
'topic': topic
}
if privacy_level is not utils.MISSING:
if not isinstance(privacy_level, StagePrivacyLevel):
raise InvalidArgument('privacy_level field must be of type PrivacyLevel')
payload['privacy_level'] = privacy_level.value
data = await self._state.http.create_stage_instance(**payload)
return StageInstance(guild=self.guild, state=self._state, data=data)
async def fetch_instance(self) -> StageInstance:
"""|coro|
Gets the running :class:`StageInstance`.
.. versionadded:: 2.0
Raises
-------
:exc:`.NotFound`
The stage instance or channel could not be found.
:exc:`.HTTPException`
Getting the stage instance failed.
Returns
--------
:class:`StageInstance`
The stage instance.
"""
data = await self._state.http.get_stage_instance(self.id)
return StageInstance(guild=self.guild, state=self._state, data=data)
@overload
async def edit(
@ -918,12 +1018,13 @@ class StageChannel(VocalGuildChannel):
You must have the :attr:`~Permissions.manage_channels` permission to
use this.
.. versionchanged:: 2.0
The ``topic`` parameter must now be set via :attr:`create_instance`.
Parameters
----------
name: :class:`str`
The new channel's name.
topic: Optional[:class:`str`]
The new channel's topic.
position: :class:`int`
The new channel's position.
sync_permissions: :class:`bool`

55
discord/client.py

@ -29,7 +29,7 @@ import logging
import signal
import sys
import traceback
from typing import Any, List, Optional, TYPE_CHECKING, Union
from typing import Any, Generator, List, Optional, TYPE_CHECKING, TypeVar, Union
import aiohttp
@ -56,6 +56,7 @@ from .webhook import Webhook
from .iterators import GuildIterator
from .appinfo import AppInfo
from .ui.view import View
from .stage_instance import StageInstance
__all__ = (
'Client',
@ -693,6 +694,28 @@ class Client:
"""
return self._connection.get_channel(id)
def get_stage_instance(self, id) -> Optional[StageInstance]:
"""Returns a stage instance with the given stage channel ID.
.. versionadded:: 2.0
Parameters
-----------
id: :class:`int`
The ID to search for.
Returns
--------
Optional[:class:`StageInstance`]
The returns stage instance of ``None`` if not found.
"""
from .channel import StageChannel
channel = self._connection.get_channel(id)
if isinstance(channel, StageChannel):
return channel.instance
def get_guild(self, id):
"""Returns a guild with the given ID.
@ -1136,6 +1159,34 @@ class Client:
data = await self.http.create_guild(name, region_value, icon)
return Guild(data=data, state=self._connection)
async def fetch_stage_instance(self, channel_id: int) -> StageInstance:
"""|coro|
Gets a :class:`StageInstance` for a stage channel id.
.. versionadded:: 2.0
Parameters
-----------
channel_id: :class:`int`
The stage channel ID.
Raises
-------
:exc:`.NotFound`
The stage instance or channel could not be found.
:exc:`.HTTPException`
Getting the stage instance failed.
Returns
--------
:class:`StageInstance`
The stage instance from the stage channel ID.
"""
data = await self.http.get_stage_instance(channel_id)
guild = self.get_guild(int(data['guild_id']))
return StageInstance(guild=guild, state=self._connection, data=data) # type: ignore
# Invite management
async def fetch_invite(self, url: Union[Invite, str], *, with_counts: bool = True, with_expiration: bool = True) -> Invite:
@ -1261,7 +1312,7 @@ class Client:
async def fetch_user(self, user_id):
"""|coro|
Retrieves a :class:`~discord.User` based on their ID.
Retrieves a :class:`~discord.User` based on their ID.
You do not have to share any guilds with the user to get this information,
however many operations do require that you do.

6
discord/enums.py

@ -50,6 +50,7 @@ __all__ = (
'VideoQualityMode',
'ComponentType',
'ButtonStyle',
'StagePrivacyLevel',
)
def _create_value_cls(name):
@ -480,6 +481,11 @@ class ButtonStyle(Enum):
def __int__(self):
return self.value
class StagePrivacyLevel(Enum):
public = 1
closed = 2
guild_only = 2
T = TypeVar('T')
def create_unknown_value(cls: Type[T], val: Any) -> T:

36
discord/guild.py

@ -46,6 +46,7 @@ from .widget import Widget
from .asset import Asset
from .flags import SystemChannelFlags
from .integrations import Integration, _integration_factory
from .stage_instance import StageInstance
__all__ = (
'Guild',
@ -182,7 +183,7 @@ class Guild(Hashable):
'description', 'max_presences', 'max_members', 'max_video_channel_users',
'premium_tier', 'premium_subscription_count', '_system_channel_flags',
'preferred_locale', '_discovery_splash', '_rules_channel_id',
'_public_updates_channel_id', 'nsfw')
'_public_updates_channel_id', '_stage_instances', 'nsfw')
_PREMIUM_GUILD_LIMITS = {
None: _GuildLimit(emoji=50, bitrate=96e3, filesize=8388608),
@ -319,6 +320,11 @@ class Guild(Hashable):
self._public_updates_channel_id = utils._get_as_snowflake(guild, 'public_updates_channel_id')
self.nsfw = guild.get('nsfw', False)
self._stage_instances = {}
for s in guild.get('stage_instances', []):
stage_instance = StageInstance(guild=self, data=s, state=state)
self._stage_instances[stage_instance.id] = stage_instance
cache_joined = self._state.member_cache_flags.joined
self_id = self._state.self_id
for mdata in guild.get('members', []):
@ -613,6 +619,32 @@ class Guild(Hashable):
return role
return None
@property
def stage_instances(self) -> List[StageInstance]:
"""List[:class:`StageInstance`]: Returns a :class:`list` of the guild's stage instances that
are currently running.
.. versionadded:: 2.0
"""
return list(self._stage_instances.values())
def get_stage_instance(self, stage_instance_id: int) -> Optional[StageInstance]:
"""Returns a stage instance with the given ID.
.. versionadded:: 2.0
Parameters
-----------
stage_instance_id: :class:`int`
The ID to search for.
Returns
--------
Optional[:class:`StageInstance`]
The stage instance or ``None`` if not found.
"""
return self._stage_instances.get(stage_instance_id)
@property
def owner(self):
"""Optional[:class:`Member`]: The member that owns the guild."""
@ -1801,7 +1833,7 @@ class Guild(Hashable):
The list of integrations that are attached to the guild.
"""
data = await self._state.http.get_all_integrations(self.id)
def convert(d):
factory, _ = _integration_factory(d['type'])
if factory is None:

29
discord/http.py

@ -44,7 +44,9 @@ if TYPE_CHECKING:
from .types import (
interactions,
invite,
stage_instance,
)
from .types.snowflake import Snowflake
T = TypeVar('T')
Response = Coroutine[Any, Any, T]
@ -1080,6 +1082,33 @@ class HTTPClient:
def move_member(self, user_id, guild_id, channel_id, *, reason=None):
return self.edit_member(guild_id=guild_id, user_id=user_id, channel_id=channel_id, reason=reason)
# Stage instance management
def get_stage_instance(self, channel_id: Snowflake) -> Response[stage_instance.StageInstance]:
return self.request(Route('GET', '/stage-instances/{channel_id}', channel_id=channel_id))
def create_stage_instance(self, **payload) -> Response[stage_instance.StageInstance]:
valid_keys = (
'channel_id',
'topic',
'privacy_level',
)
payload = {k: v for k, v in payload.items() if k in valid_keys}
return self.request(Route('POST', '/stage-instances'), json=payload)
def edit_stage_instance(self, channel_id: Snowflake, **payload) -> Response[None]:
valid_keys = (
'topic',
'privacy_level',
)
payload = {k: v for k, v in payload.items() if k in valid_keys}
return self.request(Route('PATCH', '/stage-instances/{channel_id}', channel_id=channel_id), json=payload)
def delete_stage_instance(self, channel_id: Snowflake) -> Response[None]:
return self.request(Route('DELETE', '/stage-instances/{channel_id}', channel_id=channel_id))
# Application commands (global)
def get_global_commands(self, application_id) -> Response[List[interactions.ApplicationCommand]]:

168
discord/stage_instance.py

@ -0,0 +1,168 @@
"""
The MIT License (MIT)
Copyright (c) 2015-present Rapptz
Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"),
to deal in the Software without restriction, including without limitation
the rights to use, copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
"""
from __future__ import annotations
from typing import Optional, TYPE_CHECKING
from .utils import MISSING, cached_slot_property
from .mixins import Hashable
from .errors import InvalidArgument
from .enums import StagePrivacyLevel, try_enum
__all__ = (
'StageInstance',
)
if TYPE_CHECKING:
from .types.channel import StageInstance as StageInstancePayload
from .state import ConnectionState
from .channel import StageChannel
from .guild import Guild
class StageInstance(Hashable):
"""Represents a stage instance of a stage channel in a guild.
.. versionadded:: 2.0
.. container:: operations
.. describe:: x == y
Checks if two stagea instances are equal.
.. describe:: x != y
Checks if two stage instances are not equal.
.. describe:: hash(x)
Returns the stage instance's hash.
Attributes
-----------
id: :class:`int`
The stage instance's ID.
guild: :class:`Guild`
The guild that the stage instance is running in.
channel_id: :class:`int`
The ID of the channel that the stage instance is running in.
topic: :class:`str`
The topic of the stage instance.
privacy_level: :class:`StagePrivacyLevel`
The privacy level of the stage instance.
discoverable_disabled: :class:`bool`
Whether the stage instance is discoverable.
"""
__slots__ = (
'_state',
'id',
'guild',
'channel_id',
'topic',
'privacy_level',
'discoverable_disabled',
'_cs_channel',
)
def __init__(self, *, state: ConnectionState, guild: Guild, data: StageInstancePayload) -> None:
self._state = state
self.guild = guild
self._update(data)
def _update(self, data: StageInstancePayload):
self.id: int = int(data['id'])
self.channel_id: int = int(data['channel_id'])
self.topic: str = data['topic']
self.privacy_level = try_enum(StagePrivacyLevel, data['privacy_level'])
self.discoverable_disabled = data['discoverable_disabled']
def __repr__(self) -> str:
return f'<StageInstance id={self.id} guild={self.guild!r} channel_id={self.channel_id} topic={self.topic!r}>'
@cached_slot_property('_cs_channel')
def channel(self) -> Optional[StageChannel]:
"""Optional[:class:`StageChannel`: The guild that stage instance is running in."""
return self._state.get_channel(self.channel_id)
def is_public(self) -> bool:
return self.privacy_level is StagePrivacyLevel.public
async def edit(self, *, topic: str = MISSING, privacy_level: StagePrivacyLevel = MISSING) -> None:
"""|coro|
Edits the stage instance.
You must have the :attr:`~Permissions.manage_channels` permission to
use this.
Parameters
-----------
topic: :class:`str`
The stage instance's new topic.
privacy_level: :class:`StagePrivacyLevel`
The stage instance's new privacy level.
Raises
------
InvalidArgument
If the ``privacy_level`` parameter is not the proper type.
Forbidden
You do not have permissions to edit the stage instance.
HTTPException
Editing a stage instance failed.
"""
payload = {}
if topic is not MISSING:
payload['topic'] = topic
if privacy_level is not MISSING:
if not isinstance(privacy_level, StagePrivacyLevel):
raise InvalidArgument('privacy_level field must be of type PrivacyLevel')
payload['privacy_level'] = privacy_level.value
if payload:
await self._state.http.edit_stage_instance(self.channel_id, **payload)
async def delete(self) -> None:
"""|coro|
Deletes the stage instance.
You must have the :attr:`~Permissions.manage_channels` permission to
use this.
Raises
------
Forbidden
You do not have permissions to delete the stage instance.
HTTPException
Deleting the stage instance failed.
"""
await self._state.http.delete_stage_instance(self.channel_id)

35
discord/state.py

@ -53,6 +53,7 @@ from .object import Object
from .invite import Invite
from .interactions import Interaction
from .ui.view import ViewStore
from .stage_instance import StageInstance
class ChunkRequest:
def __init__(self, guild_id, loop, resolver, *, cache=True):
@ -956,6 +957,40 @@ class ConnectionState:
else:
log.debug('WEBHOOKS_UPDATE referencing an unknown channel ID: %s. Discarding.', data['channel_id'])
def parse_stage_instance_create(self, data):
guild = self._get_guild(int(data['guild_id']))
if guild is not None:
stage_instance = StageInstance(guild=guild, state=self, data=data)
guild._stage_instances[stage_instance.id] = stage_instance
self.dispatch('stage_instance_create', stage_instance)
else:
log.debug('STAGE_INSTANCE_CREATE referencing unknown guild ID: %s. Discarding.', data['guild_id'])
def parse_stage_instance_update(self, data):
guild = self._get_guild(int(data['guild_id']))
if guild is not None:
stage_instance = guild._stage_instances.get(int(data['id']))
if stage_instance is not None:
old_stage_instance = copy.copy(stage_instance)
stage_instance._update(data)
self.dispatch('stage_instance_update', old_stage_instance, stage_instance)
else:
log.debug('STAGE_INSTANCE_UPDATE referencing unknown stage instance ID: %s. Discarding.', data['id'])
else:
log.debug('STAGE_INSTANCE_UPDATE referencing unknown guild ID: %s. Discarding.', data['guild_id'])
def parse_stage_instance_delete(self, data):
guild = self._get_guild(int(data['guild_id']))
if guild is not None:
try:
stage_instance = guild._stage_instances.pop(int(data['id']))
except KeyError:
pass
else:
self.dispatch('stage_instance_delete', stage_instance)
else:
log.debug('STAGE_INSTANCE_DELETE referencing unknown guild ID: %s. Discarding.', data['guild_id'])
def parse_voice_state_update(self, data):
guild = self._get_guild(utils._get_as_snowflake(data, 'guild_id'))
channel_id = utils._get_as_snowflake(data, 'channel_id')

12
discord/types/channel.py

@ -89,3 +89,15 @@ class DMChannel(PartialChannel):
class GroupDMChannel(DMChannel):
icon: Optional[str]
owner_id: Snowflake
PrivacyLevel = Literal[1, 2]
class StageInstance(TypedDict):
id: Snowflake
guild_id: Snowflake
channel_id: Snowflake
topic: str
privacy_level: PrivacyLevel
discoverable_disabled: bool

52
docs/api.rst

@ -835,6 +835,32 @@ to handle it, which defaults to print a traceback and ignoring the exception.
:param after: The voice state after the changes.
:type after: :class:`VoiceState`
.. function:: on_stage_instance_create(stage_instance)
on_stage_instance_delete(stage_instance)
Called when a :class:`StageInstance` is created or deleted for a :class:`StageChannel`.
.. versionadded:: 2.0
:param stage_instance: The stage instance that was created or deleted.
:type stage_instance: :class:`StageInstance`
.. function:: on_stage_instance_update(before, after)
Called when a :class:`StageInstance` is updated.
The following, but not limited to, examples illustrate when this event is called:
- The topic is changed.
- The privacy level is changed.
.. versionadded:: 2.0
:param before: The stage instance before the update.
:type before: :class:`StageInstance`
:param after: The stage instance after the update.
:type after: :class:`StageInstance`
.. function:: on_member_ban(guild, user)
Called when user gets banned from a :class:`Guild`.
@ -2120,6 +2146,23 @@ of :class:`enum.Enum`.
Represents full camera video quality.
.. class:: PrivacyLevel
Represents a stage instance's privacy level.
.. versionadded:: 2.0
.. attribute:: public
The stage instance can be joined by external users.
.. attribute:: closed
The stage instance can only be joined by members of the guild.
.. attribute:: guild_only
Alias for :attr:`.closed`
Async Iterator
----------------
@ -3126,6 +3169,15 @@ StageChannel
:members:
:inherited-members:
StageInstance
~~~~~~~~~~~~~~
.. attributetable:: StageInstance
.. autoclass:: StageInstance()
:members:
CategoryChannel
~~~~~~~~~~~~~~~~~

Loading…
Cancel
Save