Browse Source

Implement student hubs and directory channels (#561)

pull/10109/head
dolfies 2 years ago
committed by GitHub
parent
commit
29df9a4505
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      discord/__init__.py
  2. 423
      discord/channel.py
  3. 96
      discord/client.py
  4. 192
      discord/directory.py
  5. 35
      discord/enums.py
  6. 25
      discord/experiment.py
  7. 207
      discord/guild.py
  8. 128
      discord/http.py
  9. 32
      discord/scheduled_event.py
  10. 25
      discord/types/channel.py
  11. 81
      discord/types/directory.py
  12. 18
      discord/types/guild.py
  13. 52
      discord/types/hub.py
  14. 4
      discord/types/invite.py
  15. 1
      discord/types/scheduled_event.py
  16. 5
      discord/types/webhook.py
  17. 6
      discord/webhook/async_.py
  18. 93
      docs/api.rst

1
discord/__init__.py

@ -34,6 +34,7 @@ from .colour import *
from .commands import *
from .components import *
from .connections import *
from .directory import *
from .embeds import *
from .emoji import *
from .entitlements import *

423
discord/channel.py

@ -47,7 +47,17 @@ import datetime
import discord.abc
from .scheduled_event import ScheduledEvent
from .permissions import PermissionOverwrite, Permissions
from .enums import ChannelType, EntityType, ForumLayoutType, ForumOrderType, PrivacyLevel, try_enum, VideoQualityMode
from .enums import (
ChannelType,
EntityType,
ForumLayoutType,
ForumOrderType,
PrivacyLevel,
try_enum,
VideoQualityMode,
DirectoryCategory,
DirectoryEntryType,
)
from .calls import PrivateCall, GroupCall
from .mixins import Hashable
from . import utils
@ -61,15 +71,17 @@ from .flags import ChannelFlags
from .http import handle_message_parameters
from .invite import Invite
from .voice_client import VoiceClient
from .directory import DirectoryEntry
__all__ = (
'TextChannel',
'VoiceChannel',
'StageChannel',
'DMChannel',
'CategoryChannel',
'ForumTag',
'ForumChannel',
'DirectoryChannel',
'DMChannel',
'GroupChannel',
'PartialMessageable',
)
@ -98,6 +110,7 @@ if TYPE_CHECKING:
NewsChannel as NewsChannelPayload,
VoiceChannel as VoiceChannelPayload,
StageChannel as StageChannelPayload,
DirectoryChannel as DirectoryChannelPayload,
DMChannel as DMChannelPayload,
CategoryChannel as CategoryChannelPayload,
GroupDMChannel as GroupChannelPayload,
@ -2093,6 +2106,26 @@ class CategoryChannel(discord.abc.GuildChannel, Hashable):
r.sort(key=lambda c: (c.position, c.id))
return r
@property
def directory_channels(self) -> List[DirectoryChannel]:
"""List[:class:`DirectoryChannel`]: Returns the directory channels that are under this category.
.. versionadded:: 2.1
"""
r = [c for c in self.guild.channels if c.category_id == self.id and isinstance(c, DirectoryChannel)]
r.sort(key=lambda c: (c.position, c.id))
return r
@property
def directories(self) -> List[DirectoryChannel]:
"""List[:class:`DirectoryChannel`]: Returns the directory channels that are under this category.
An alias for :attr:`directory_channels`.
.. versionadded:: 2.1
"""
return self.directory_channels
async def create_text_channel(self, name: str, **options: Any) -> TextChannel:
"""|coro|
@ -2131,6 +2164,22 @@ class CategoryChannel(discord.abc.GuildChannel, Hashable):
"""
return await self.guild.create_stage_channel(name, category=self, **options)
async def create_directory(self, name: str, **options: Any) -> DirectoryChannel:
"""|coro|
A shortcut method to :meth:`Guild.create_directory` to create a :class:`DirectoryChannel` in the category.
.. versionadded:: 2.1
Returns
--------
:class:`DirectoryChannel`
The channel that was just created.
"""
return await self.guild.create_directory(name, category=self, **options)
create_directory_channel = create_directory
async def create_forum(self, name: str, **options: Any) -> ForumChannel:
"""|coro|
@ -2145,6 +2194,8 @@ class CategoryChannel(discord.abc.GuildChannel, Hashable):
"""
return await self.guild.create_forum(name, category=self, **options)
create_forum_channel = create_forum
class ForumTag(Hashable):
"""Represents a forum tag that can be applied to a thread within a :class:`ForumChannel`.
@ -2999,6 +3050,372 @@ class ForumChannel(discord.abc.GuildChannel, Hashable):
before_timestamp = update_before(threads[-1])
class DirectoryChannel(discord.abc.GuildChannel, Hashable):
"""Represents a directory channel.
These channels hold entries for guilds attached to a directory (such as a Student Hub).
.. 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.
.. versionadded:: 2.1
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.
topic: Optional[:class:`str`]
The channel's topic. ``None`` if it doesn't exist.
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.
last_message_id: Optional[:class:`int`]
The last directory entry ID that was created on this channel. It may
*not* point to an existing or valid directory entry.
"""
__slots__ = (
'name',
'id',
'guild',
'topic',
'_state',
'category_id',
'position',
'_overwrites',
'last_message_id',
)
def __init__(self, *, state: ConnectionState, guild: Guild, data: DirectoryChannelPayload):
self._state: ConnectionState = state
self.id: int = int(data['id'])
self._update(guild, data)
def __repr__(self) -> str:
attrs = [
('id', self.id),
('name', self.name),
('position', self.position),
('category_id', self.category_id),
]
joined = ' '.join('%s=%r' % t for t in attrs)
return f'<{self.__class__.__name__} {joined}>'
def _update(self, guild: Guild, data: DirectoryChannelPayload) -> None:
self.guild: Guild = guild
self.name: str = data['name']
self.category_id: Optional[int] = utils._get_as_snowflake(data, 'parent_id')
self.topic: Optional[str] = data.get('topic')
self.position: int = data['position']
self.last_message_id: Optional[int] = utils._get_as_snowflake(data, 'last_message_id')
self._fill_overwrites(data)
async def _get_channel(self) -> Self:
return self
@property
def type(self) -> ChannelType:
""":class:`ChannelType`: The channel's Discord type."""
return ChannelType.directory
@property
def _sorting_bucket(self) -> int:
return ChannelType.directory.value
@property
def _scheduled_event_entity_type(self) -> Optional[EntityType]:
return None
@utils.copy_doc(discord.abc.GuildChannel.permissions_for)
def permissions_for(self, obj: Union[Member, Role], /) -> Permissions:
base = super().permissions_for(obj)
self._apply_implicit_permissions(base)
# text channels do not have voice related permissions
denied = Permissions.voice()
base.value &= ~denied.value
return base
@property
def members(self) -> List[Member]:
"""List[:class:`Member`]: Returns all members that can see this channel."""
return [m for m in self.guild.members if self.permissions_for(m).read_messages]
@property
def read_state(self) -> ReadState:
""":class:`ReadState`: Returns the read state for this channel."""
return self._state.get_read_state(self.id)
@property
def acked_message_id(self) -> int:
""":class:`int`: The last directory entry ID that the user has acknowledged.
It may *not* point to an existing or valid directory entry.
"""
return self.read_state.last_acked_id
@property
def mention_count(self) -> int:
""":class:`int`: Returns how many unread directory entries the user has in this channel."""
return self.read_state.badge_count
@property
def last_viewed_timestamp(self) -> datetime.date:
""":class:`datetime.date`: When the channel was last viewed."""
return self.read_state.last_viewed # type: ignore
@overload
async def edit(self) -> Optional[DirectoryChannel]:
...
@overload
async def edit(self, *, position: int, reason: Optional[str] = ...) -> None:
...
@overload
async def edit(
self,
*,
reason: Optional[str] = ...,
name: str = ...,
topic: str = ...,
position: int = ...,
sync_permissions: bool = ...,
category: Optional[CategoryChannel] = ...,
overwrites: Mapping[OverwriteKeyT, PermissionOverwrite] = ...,
) -> DirectoryChannel:
...
async def edit(self, *, reason: Optional[str] = None, **options: Any) -> Optional[DirectoryChannel]:
"""|coro|
Edits the channel.
You must have :attr:`~Permissions.manage_channels` to do this.
Parameters
----------
name: :class:`str`
The new channel 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:`Mapping`
A :class:`Mapping` of target (either a role or a member) to
:class:`PermissionOverwrite` to apply to the channel.
Raises
------
ValueError
The new ``position`` is less than 0 or greater than the number of channels.
TypeError
The permission overwrite information is not in proper form.
Forbidden
You do not have permissions to edit the channel.
HTTPException
Editing the channel failed.
Returns
--------
Optional[:class:`.DirectoryChannel`]
The newly edited directory channel. If the edit was only positional
then ``None`` is returned instead.
"""
payload = await self._edit(options, reason=reason)
if payload is not None:
# the payload will always be the proper channel payload
return self.__class__(state=self._state, guild=self.guild, data=payload) # type: ignore
@utils.copy_doc(discord.abc.GuildChannel.clone)
async def clone(self, *, name: Optional[str] = None, reason: Optional[str] = None) -> DirectoryChannel:
return await self._clone_impl({'topic': self.topic}, name=name, reason=reason)
async def counts(self) -> Dict[DirectoryCategory, int]:
"""|coro|
Gets the number of entries in each category.
Raises
-------
Forbidden
You don't have permissions to get the counts.
HTTPException
Getting the counts failed.
Returns
--------
Dict[:class:`DirectoryCategory`, :class:`int`]
The counts for each category.
"""
data = await self._state.http.get_directory_counts(self.id)
return {try_enum(DirectoryCategory, int(k)): v for k, v in data.items()}
async def entries(
self,
*,
type: Optional[DirectoryEntryType] = None,
category: Optional[DirectoryCategory] = None,
) -> List[DirectoryEntry]:
"""|coro|
Gets the directory entries in this channel.
Raises
-------
Forbidden
You don't have permissions to get the entries.
HTTPException
Getting the entries failed.
Returns
--------
List[:class:`DirectoryEntry`]
The entries in this channel.
"""
state = self._state
data = await state.http.get_directory_entries(
self.id, type=type.value if type else None, category_id=category.value if category else None
)
return [DirectoryEntry(state=state, data=e, channel=self) for e in data]
async def fetch_entries(self, *entity_ids: int) -> List[DirectoryEntry]:
r"""|coro|
Gets a list of partial directory entries by their IDs.
.. note::
These :class:`DirectoryEntry` objects do not have :attr:`DirectoryEntry.guild`.
Parameters
-----------
\*entity_ids: :class:`int`
The IDs of the entries to fetch.
Raises
-------
Forbidden
You don't have permissions to get the entries.
HTTPException
Getting the entries failed.
Returns
--------
List[:class:`DirectoryEntry`]
The entries in this channel.
"""
if not entity_ids:
return []
state = self._state
data = await state.http.get_some_directory_entries(self.id, entity_ids)
return [DirectoryEntry(state=state, data=e, channel=self) for e in data]
async def search_entries(
self,
query: str,
/,
*,
category: Optional[DirectoryCategory] = None,
) -> List[DirectoryEntry]:
"""|coro|
Searches for directory entries in this channel.
Parameters
-----------
query: :class:`str`
The query to search for.
Raises
-------
Forbidden
You don't have permissions to search the entries.
HTTPException
Searching the entries failed.
Returns
--------
List[:class:`DirectoryEntry`]
The entries in this channel.
"""
state = self._state
data = await state.http.search_directory_entries(self.id, query, category_id=category.value if category else None)
return [DirectoryEntry(state=state, data=e, channel=self) for e in data]
async def create_entry(
self,
guild: Snowflake,
*,
category: DirectoryCategory = DirectoryCategory.uncategorized,
description: Optional[str] = None,
) -> DirectoryEntry:
"""|coro|
Creates a directory entry in this channel.
Parameters
-----------
guild: :class:`Guild`
The guild to create the entry for.
category: :class:`DirectoryCategory`
The category to create the entry in.
description: Optional[:class:`str`]
The description of the entry.
Raises
-------
Forbidden
You don't have permissions to create the entry.
HTTPException
Creating the entry failed.
Returns
--------
:class:`DirectoryEntry`
The created entry.
"""
# While the API supports `type`, only guilds seem to be supported at the moment
# So we hide all that from the user and just accept a `guild`
state = self._state
data = await state.http.create_directory_entry(
self.id,
guild.id,
primary_category_id=(category or DirectoryCategory.uncategorized).value,
description=description or '',
)
return DirectoryEntry(state=state, data=data, channel=self)
class DMChannel(discord.abc.Messageable, discord.abc.Connectable, discord.abc.PrivateChannel, Hashable):
"""Represents a Discord direct message channel.
@ -4118,6 +4535,8 @@ def _guild_channel_factory(channel_type: int):
return TextChannel, value
elif value is ChannelType.stage_voice:
return StageChannel, value
elif value is ChannelType.directory:
return DirectoryChannel, value
elif value is ChannelType.forum:
return ForumChannel, value
else:

96
discord/client.py

@ -5064,6 +5064,102 @@ class Client:
return experiments
async def join_hub_waitlist(self, email: str, school: str) -> None:
"""|coro|
Signs up for the Discord Student Hub waitlist.
.. versionadded:: 2.1
Parameters
-----------
email: :class:`str`
The email to sign up with.
school: :class:`str`
The school name to sign up with.
Raises
-------
HTTPException
Signing up for the waitlist failed.
"""
await self._connection.http.hub_waitlist_signup(email, school)
async def lookup_hubs(self, email: str, /) -> List[Guild]:
"""|coro|
Looks up the Discord Student Hubs for the given email.
.. note::
Using this, you will only receive
:attr:`.Guild.id`, :attr:`.Guild.name`, and :attr:`.Guild.icon` per guild.
.. versionadded:: 2.1
Parameters
-----------
email: :class:`str`
The email to look up.
Raises
-------
HTTPException
Looking up the hubs failed.
Returns
--------
List[:class:`.Guild`]
The hubs found.
"""
state = self._connection
data = await state.http.hub_lookup(email)
return [Guild(state=state, data=d) for d in data.get('guilds_info', [])] # type: ignore
@overload
async def join_hub(self, guild: Snowflake, email: str, *, code: None = ...) -> None:
...
@overload
async def join_hub(self, guild: Snowflake, email: str, *, code: str = ...) -> Guild:
...
async def join_hub(self, guild: Snowflake, email: str, *, code: Optional[str] = None) -> Optional[Guild]:
"""|coro|
Joins the user to or requests a verification code for a Student Hub.
.. versionadded:: 2.1
Parameters
----------
guild: :class:`.Guild`
The hub to join.
email: :class:`str`
The email to join with.
code: Optional[:class:`str`]
The email verification code.
.. note::
If not provided, this method requests a verification code instead.
Raises
------
HTTPException
Joining the hub or requesting the verification code failed.
"""
state = self._connection
if not code:
data = await state.http.hub_lookup(email, guild.id)
if not data.get('has_matching_guild'):
raise ValueError('Guild does not match email')
return
data = await state.http.join_hub(email, guild.id, code)
return Guild(state=state, data=data['guild'])
async def pomelo_suggestion(self) -> str:
"""|coro|

192
discord/directory.py

@ -0,0 +1,192 @@
"""
The MIT License (MIT)
Copyright (c) 2021-present Dolfies
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 TYPE_CHECKING, Optional, Union
from .enums import DirectoryCategory, DirectoryEntryType, try_enum
from .scheduled_event import ScheduledEvent
from .utils import MISSING, parse_time
if TYPE_CHECKING:
from datetime import datetime
from .channel import DirectoryChannel
from .member import Member
from .state import ConnectionState
from .types.directory import (
DirectoryEntry as DirectoryEntryPayload,
PartialDirectoryEntry as PartialDirectoryEntryPayload,
)
__all__ = ('DirectoryEntry',)
class DirectoryEntry:
"""Represents a directory entry for a channel.
.. container:: operations
.. describe:: x == y
Checks if two entries are equal.
.. describe:: x != y
Checks if two entries are not equal.
.. describe:: hash(x)
Returns the entry's hash.
.. versionadded:: 2.1
Attributes
-----------
channel: :class:`DirectoryChannel`
The channel this entry is from.
type: :class:`DirectoryEntryType`
The type of this entry.
category: :class:`DirectoryCategory`
The primary category of this entry.
author_id: :class:`int`
The ID of the user who created this entry.
created_at: :class:`datetime.datetime`
When this entry was created.
description: Optional[:class:`str`]
The description of the entry's guild.
Only applicable for entries of type :attr:`DirectoryEntryType.guild`.
entity_id: :class:`int`
The ID of the entity this entry represents.
guild: Optional[:class:`Guild`]
The guild this entry represents.
For entries of type :attr:`DirectoryEntryType.scheduled_event`,
this is the guild the scheduled event is from.
Not available in all contexts.
featurable: :class:`bool`
Whether this entry's guild can be featured in the directory.
Only applicable for entries of type :attr:`DirectoryEntryType.guild`.
scheduled_event: Optional[:class:`ScheduledEvent`]
The scheduled event this entry represents.
Only applicable for entries of type :attr:`DirectoryEntryType.scheduled_event`.
rsvp: :class:`bool`
Whether the current user has RSVP'd to the scheduled event.
Only applicable for entries of type :attr:`DirectoryEntryType.scheduled_event`.
"""
def __init__(
self,
*,
data: Union[DirectoryEntryPayload, PartialDirectoryEntryPayload],
state: ConnectionState,
channel: DirectoryChannel,
):
self.channel = channel
self._state = state
self._update(data)
def __repr__(self) -> str:
return f'<DirectoryEntry channel={self.channel!r} type={self.type!r} category={self.category!r} author_id={self.author_id!r} guild={self.guild!r}>'
def __hash__(self) -> int:
return hash((self.channel.id, self.entity_id))
def __eq__(self, other: object) -> bool:
if isinstance(other, DirectoryEntry):
return self.channel == other.channel and self.entity_id == other.entity_id
return NotImplemented
def _update(self, data: Union[DirectoryEntryPayload, PartialDirectoryEntryPayload]):
from .guild import Guild
state = self._state
self.type: DirectoryEntryType = try_enum(DirectoryEntryType, data['type'])
self.category: DirectoryCategory = try_enum(DirectoryCategory, data.get('primary_category_id', 0))
self.author_id: int = int(data['author_id'])
self.created_at: datetime = parse_time(data['created_at'])
self.description: Optional[str] = data.get('description') or None
self.entity_id: int = int(data['entity_id'])
guild_data = data.get('guild', data.get('guild_scheduled_event', {}).get('guild'))
self.guild: Optional[Guild] = Guild(data=guild_data, state=state) if guild_data is not None else None
self.featurable: bool = guild_data.get('featurable_in_directory', False) if guild_data is not None else False
event_data = data.get('guild_scheduled_event')
self.scheduled_event: Optional[ScheduledEvent] = (
ScheduledEvent(data=event_data, state=state) if event_data is not None else None
)
self.rsvp: bool = event_data.get('user_rsvp', False) if event_data is not None else False
@property
def author(self) -> Optional[Member]:
"""Optional[:class:`Member`]: The member that created this entry."""
return self.channel.guild.get_member(self.author_id)
async def edit(self, *, description: Optional[str] = MISSING, category: DirectoryCategory = MISSING) -> None:
"""|coro|
Edits this directory entry.
Only entries of type :attr:`DirectoryEntryType.guild` can be edited.
You must be the author of the entry or have
:attr:`~Permissions.manage_guild` in the represented guild to edit it.
Parameters
-----------
description: Optional[:class:`str`]
The new description of the entry's guild.
category: :class:`DirectoryCategory`
The new primary category of the entry.
Raises
-------
Forbidden
You do not have permissions to edit this entry.
HTTPException
Editing the entry failed.
"""
data = await self._state.http.edit_directory_entry(
self.channel.id,
self.entity_id,
description=description,
primary_category_id=category.value if category is not MISSING else MISSING,
)
self._update(data)
async def delete(self) -> None:
"""|coro|
Deletes this directory entry.
You must be the author of the entry or have
:attr:`~Permissions.manage_guild` in the represented guild to delete it.
Raises
-------
Forbidden
You do not have permissions to delete this entry.
HTTPException
Deleting the entry failed.
"""
await self._state.http.delete_directory_entry(self.channel.id, self.entity_id)

35
discord/enums.py

@ -25,7 +25,7 @@ from __future__ import annotations
import types
from collections import namedtuple
from typing import Any, ClassVar, Dict, List, Optional, TYPE_CHECKING, Tuple, Type, TypeVar, Iterator, Mapping
from typing import TYPE_CHECKING, Any, ClassVar, Dict, Iterator, List, Mapping, Optional, Tuple, Type, TypeVar
__all__ = (
'Enum',
@ -119,6 +119,9 @@ __all__ = (
'ForumLayoutType',
'ForumOrderType',
'ReadStateType',
'DirectoryEntryType',
'DirectoryCategory',
'HubType',
)
if TYPE_CHECKING:
@ -253,6 +256,7 @@ class ChannelType(Enum):
public_thread = 11
private_thread = 12
stage_voice = 13
directory = 14
forum = 15
def __str__(self) -> str:
@ -911,6 +915,9 @@ class PrivacyLevel(Enum):
closed = 2
guild_only = 2
def __int__(self) -> int:
return self.value
class ScheduledEventEntityType(Enum):
stage_instance = 1
@ -1566,6 +1573,32 @@ class ReadStateType(Enum):
onboarding = 4
class DirectoryEntryType(Enum):
guild = 0
scheduled_event = 1
def __int__(self) -> int:
return self.value
class DirectoryCategory(Enum):
uncategorized = 0
school_club = 1
class_subject = 2
study_social = 3
miscellaneous = 5
def __int__(self) -> int:
return self.value
class HubType(Enum):
default = 0
high_school = 1
college = 2
university = 2
def create_unknown_value(cls: Type[E], val: Any) -> E:
value_cls = cls._enum_value_cls_ # type: ignore # This is narrowed below
name = f'unknown_{val}'

25
discord/experiment.py

@ -26,6 +26,7 @@ from __future__ import annotations
from typing import TYPE_CHECKING, Dict, Final, Iterator, List, Optional, Sequence, Tuple, Union
from .enums import HubType, try_enum
from .metadata import Metadata
from .utils import SequenceProxy, SnowflakeList, murmurhash32
@ -202,13 +203,12 @@ class ExperimentFilters:
if ids_filter is not None:
return ids_filter.guild_ids
# TODO: Pending hub implementation
# @property
# def hub_types(self) -> Optional[List[HubType]]:
# """Optional[List[:class:`HubType`]]: The hub types that are eligible for the population."""
# hub_types_filter = self.options.guild_hub_types
# if hub_types_filter is not None:
# return [try_enum(HubType, hub_type) for hub_type in hub_types_filter.guild_hub_types]
@property
def hub_types(self) -> Optional[List[HubType]]:
"""Optional[List[:class:`HubType`]]: The Student Hub types that are eligible for the population."""
hub_types_filter = self.options.guild_hub_types
if hub_types_filter is not None:
return [try_enum(HubType, hub_type) for hub_type in hub_types_filter.guild_hub_types]
@property
def range_by_hash(self) -> Optional[Tuple[int, int]]:
@ -265,12 +265,11 @@ class ExperimentFilters:
if guild.id not in ids:
return False
# TODO: Pending hub implementation
# hub_types = self.hub_types
# if hub_types is not None:
# # Guild must be in the list of hub types
# if not guild.hub_type or guild.hub_type not in hub_types:
# return False
hub_types = self.hub_types
if hub_types is not None:
# Guild must be a hub in the list of hub types
if not guild.hub_type or guild.hub_type not in hub_types:
return False
range_by_hash = self.range_by_hash
if range_by_hash is not None:

207
discord/guild.py

@ -63,6 +63,7 @@ from .enums import (
VideoQualityMode,
ChannelType,
EntityType,
HubType,
PrivacyLevel,
try_enum,
VerificationLevel,
@ -127,6 +128,7 @@ if TYPE_CHECKING:
CategoryChannel as CategoryChannelPayload,
StageChannel as StageChannelPayload,
ForumChannel as ForumChannelPayload,
DirectoryChannel as DirectoryChannelPayload,
)
from .types.embed import EmbedType
from .types.integration import IntegrationType
@ -139,8 +141,9 @@ if TYPE_CHECKING:
from .read_state import ReadState
VocalGuildChannel = Union[VoiceChannel, StageChannel]
GuildChannel = Union[VocalGuildChannel, ForumChannel, TextChannel, CategoryChannel]
ByCategoryItem = Tuple[Optional[CategoryChannel], List[GuildChannel]]
NonCategoryChannel = Union[VocalGuildChannel, ForumChannel, TextChannel, DirectoryChannel]
GuildChannel = Union[NonCategoryChannel, CategoryChannel]
ByCategoryItem = Tuple[Optional[CategoryChannel], List[NonCategoryChannel]]
MISSING = utils.MISSING
@ -419,6 +422,10 @@ class Guild(Hashable):
Indicates if the guild has widget enabled.
.. versionadded:: 2.0
hub_type: Optional[:class:`HubType`]
The type of Student Hub the guild is, if applicable.
.. versionadded:: 2.1
"""
__slots__ = (
@ -475,6 +482,7 @@ class Guild(Hashable):
'keywords',
'primary_category_id',
'application_command_counts',
'hub_type',
'_joined_at',
'_cs_joined',
)
@ -551,7 +559,7 @@ class Guild(Hashable):
('id', self.id),
('name', self.name),
('chunked', self.chunked),
('member_count', self._member_count),
('member_count', self.member_count),
)
inner = ' '.join('%s=%r' % t for t in attrs)
return f'<Guild {inner}>'
@ -621,6 +629,9 @@ class Guild(Hashable):
)
self.explicit_content_filter: ContentFilter = try_enum(ContentFilter, guild.get('explicit_content_filter', 0))
self.afk_timeout: int = guild.get('afk_timeout', 0)
self.hub_type: Optional[HubType] = (
try_enum(HubType, guild.get('hub_type')) if guild.get('hub_type') is not None else None
)
self.unavailable: bool = guild.get('unavailable', False)
if self.unavailable:
self._member_count = 0
@ -733,6 +744,13 @@ class Guild(Hashable):
def _offline_members_hidden(self) -> bool:
return (self._member_count or 0) > 1000
def is_hub(self) -> bool:
""":class:`bool`: Whether the guild is a Student Hub.
.. versionadded:: 2.1
"""
return 'HUB' in self.features
@property
def voice_channels(self) -> List[VoiceChannel]:
"""List[:class:`VoiceChannel`]: A list of voice channels that belongs to this guild.
@ -842,6 +860,28 @@ class Guild(Hashable):
r.sort(key=lambda c: (c.position, c.id))
return r
@property
def directory_channels(self) -> List[DirectoryChannel]:
"""List[:class:`DirectoryChannel`]: A list of directory channels that belongs to this guild.
This is sorted by the position and are in UI order from top to bottom.
.. versionadded:: 2.1
"""
r = [ch for ch in self._channels.values() if isinstance(ch, DirectoryChannel)]
r.sort(key=lambda c: (c.position, c.id))
return r
@property
def directories(self) -> List[DirectoryChannel]:
"""List[:class:`DirectoryChannel`]: A list of directory channels that belongs to this guild.
An alias for :attr:`Guild.directory_channels`.
.. versionadded:: 2.1
"""
return self.directory_channels
def by_category(self) -> List[ByCategoryItem]:
"""Returns every :class:`CategoryChannel` and their associated channels.
@ -855,7 +895,7 @@ class Guild(Hashable):
List[Tuple[Optional[:class:`CategoryChannel`], List[:class:`abc.GuildChannel`]]]:
The categories and their associated channels.
"""
grouped: Dict[Optional[int], List[GuildChannel]] = {}
grouped: Dict[Optional[int], List[NonCategoryChannel]] = {}
for channel in self._channels.values():
if isinstance(channel, CategoryChannel):
grouped.setdefault(channel.id, [])
@ -866,7 +906,7 @@ class Guild(Hashable):
except KeyError:
grouped[channel.category_id] = [channel]
def key(t: ByCategoryItem) -> Tuple[Tuple[int, int], List[GuildChannel]]:
def key(t: ByCategoryItem) -> Tuple[Tuple[int, int], List[NonCategoryChannel]]:
k, v = t
return ((k.position, k.id) if k else (-1, -1), v)
@ -1422,6 +1462,17 @@ class Guild(Hashable):
) -> Coroutine[Any, Any, ForumChannelPayload]:
...
@overload
def _create_channel(
self,
name: str,
channel_type: Literal[ChannelType.directory],
overwrites: Mapping[Union[Role, Member], PermissionOverwrite] = ...,
category: Optional[Snowflake] = ...,
**options: Any,
) -> Coroutine[Any, Any, DirectoryChannelPayload]:
...
@overload
def _create_channel(
self,
@ -1851,6 +1902,83 @@ class Guild(Hashable):
create_category_channel = create_category
async def create_directory(
self,
name: str,
*,
reason: Optional[str] = None,
category: Optional[CategoryChannel] = None,
position: int = MISSING,
topic: str = MISSING,
overwrites: Mapping[Union[Role, Member], PermissionOverwrite] = MISSING,
) -> DirectoryChannel:
"""|coro|
This is similar to :meth:`create_text_channel` except makes a :class:`DirectoryChannel` instead.
The ``overwrites`` parameter can be used to create a 'secret'
channel upon creation. This parameter expects a :class:`dict` of
overwrites with the target (either a :class:`Member` or a :class:`Role`)
as the key and a :class:`PermissionOverwrite` as the value.
.. versionadded:: 2.1
Parameters
-----------
name: :class:`str`
The channel's name.
overwrites: Dict[Union[:class:`Role`, :class:`Member`], :class:`PermissionOverwrite`]
A :class:`dict` of target (either a role or a member) to
:class:`PermissionOverwrite` to apply upon creation of a channel.
Useful for creating secret channels.
category: Optional[:class:`CategoryChannel`]
The category to place the newly created channel under.
The permissions will be automatically synced to category if no
overwrites are provided.
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.
topic: :class:`str`
The new channel's topic.
reason: Optional[:class:`str`]
The reason for creating this channel. Shows up on the audit log.
Raises
-------
Forbidden
You do not have the proper permissions to create this channel.
HTTPException
Creating the channel failed.
TypeError
The permission overwrite information is not in proper form.
Returns
-------
:class:`TextChannel`
The channel that was just created.
"""
options = {}
if position is not MISSING:
options['position'] = position
if topic is not MISSING:
options['topic'] = topic
data = await self._create_channel(
name,
overwrites=overwrites,
channel_type=ChannelType.directory,
category=category,
reason=reason,
**options,
)
channel = DirectoryChannel(state=self._state, guild=self, data=data)
# temporarily add to the cache
self._channels[channel.id] = channel
return channel
create_directory_channel = create_directory
async def create_forum(
self,
name: str,
@ -1990,6 +2118,8 @@ class Guild(Hashable):
self._channels[channel.id] = channel
return channel
create_forum_channel = create_forum
async def leave(self) -> None:
"""|coro|
@ -3367,6 +3497,7 @@ class Guild(Hashable):
end_time: datetime = ...,
description: str = ...,
image: bytes = ...,
directory_broadcast: bool = ...,
reason: Optional[str] = ...,
) -> ScheduledEvent:
...
@ -3383,6 +3514,7 @@ class Guild(Hashable):
end_time: datetime = ...,
description: str = ...,
image: bytes = ...,
directory_broadcast: bool = ...,
reason: Optional[str] = ...,
) -> ScheduledEvent:
...
@ -3398,6 +3530,7 @@ class Guild(Hashable):
end_time: datetime = ...,
description: str = ...,
image: bytes = ...,
directory_broadcast: bool = ...,
reason: Optional[str] = ...,
) -> ScheduledEvent:
...
@ -3413,6 +3546,7 @@ class Guild(Hashable):
end_time: datetime = ...,
description: str = ...,
image: bytes = ...,
directory_broadcast: bool = ...,
reason: Optional[str] = ...,
) -> ScheduledEvent:
...
@ -3423,12 +3557,13 @@ class Guild(Hashable):
name: str,
start_time: datetime,
entity_type: EntityType = MISSING,
privacy_level: PrivacyLevel = MISSING,
privacy_level: PrivacyLevel = PrivacyLevel.guild_only,
channel: Optional[Snowflake] = MISSING,
location: str = MISSING,
end_time: datetime = MISSING,
description: str = MISSING,
image: bytes = MISSING,
directory_broadcast: bool = False,
reason: Optional[str] = None,
) -> ScheduledEvent:
r"""|coro|
@ -3474,6 +3609,11 @@ class Guild(Hashable):
The location of the scheduled event.
Required if the ``entity_type`` is :attr:`EntityType.external`.
directory_broadcast: :class:`bool`
Whether to broadcast the scheduled event to the directories the guild is in.
You should first check eligibility with :meth:`directory_broadcast_eligibility`.
.. versionadded:: 2.1
reason: Optional[:class:`str`]
The reason for creating this scheduled event. Shows up on the audit log.
@ -3497,17 +3637,23 @@ class Guild(Hashable):
:class:`ScheduledEvent`
The created scheduled event.
"""
payload = {}
payload: Dict[str, Any] = {
'name': name,
'privacy_level': int(privacy_level or PrivacyLevel.guild_only.value),
'broadcast_to_directory_channels': directory_broadcast,
}
metadata = {}
payload['name'] = name
if start_time.tzinfo is None:
raise ValueError(
'start_time must be an aware datetime. Consider using discord.utils.utcnow() or datetime.datetime.now().astimezone() for local time.'
)
payload['scheduled_start_time'] = start_time.isoformat()
if start_time is not MISSING:
if start_time.tzinfo is None:
raise ValueError(
'start_time must be an aware datetime. Consider using discord.utils.utcnow() or datetime.datetime.now().astimezone() for local time.'
)
payload['scheduled_start_time'] = start_time.isoformat()
if privacy_level:
if not isinstance(privacy_level, PrivacyLevel):
raise TypeError('privacy_level must be of type PrivacyLevel')
payload['privacy_level'] = (privacy_level or PrivacyLevel.guild_only).value
entity_type = entity_type or getattr(channel, '_scheduled_event_entity_type', MISSING)
if entity_type is MISSING:
@ -3516,7 +3662,6 @@ class Guild(Hashable):
entity_type = EntityType.voice
elif channel.type is StageChannel:
entity_type = EntityType.stage_instance
elif location not in (MISSING, None):
entity_type = EntityType.external
else:
@ -3527,15 +3672,9 @@ class Guild(Hashable):
if entity_type is None:
raise TypeError(
'invalid GuildChannel type passed, must be VoiceChannel or StageChannel ' f'not {channel.__class__.__name__}'
f'invalid GuildChannel type passed; must be VoiceChannel or StageChannel not {channel.__class__.__name__}'
)
if privacy_level is not MISSING:
if not isinstance(privacy_level, PrivacyLevel):
raise TypeError('privacy_level must be of type PrivacyLevel.')
payload['privacy_level'] = privacy_level.value
if description is not MISSING:
payload['description'] = description
@ -3560,10 +3699,10 @@ class Guild(Hashable):
metadata['location'] = location
if end_time in (MISSING, None):
if not end_time:
raise TypeError('end_time must be set when entity_type is external')
if end_time not in (MISSING, None):
if end_time:
if end_time.tzinfo is None:
raise ValueError(
'end_time must be an aware datetime. Consider using discord.utils.utcnow() or datetime.datetime.now().astimezone() for local time.'
@ -5135,3 +5274,23 @@ class Guild(Hashable):
"""
data = await self._state.http.migrate_command_scope(self.id)
return list(map(int, data['integration_ids_with_app_commands']))
async def directory_broadcast_eligibility(self) -> bool:
"""|coro|
Checks if scheduled events can be broadcasted to the directories the guild is in.
.. versionadded:: 2.1
Raises
-------
HTTPException
Checking eligibility failed.
Returns
--------
:class:`bool`
Whether the guild is eligible to broadcast scheduled events to directories.
"""
data = await self._state.http.get_directory_broadcast_info(self.id, 1)
return data['can_broadcast']

128
discord/http.py

@ -89,10 +89,12 @@ if TYPE_CHECKING:
automod,
billing,
channel,
directory,
emoji,
entitlements,
experiment,
guild,
hub,
integration,
invite,
library,
@ -1592,6 +1594,100 @@ class HTTPClient:
return self.request(Route('DELETE', '/channels/{channel_id}', channel_id=channel_id), params=params, reason=reason)
def get_directory_entries(
self,
channel_id: Snowflake,
*,
type: Optional[directory.DirectoryEntryType] = None,
category_id: Optional[directory.DirectoryCategory] = None,
) -> Response[List[directory.DirectoryEntry]]:
params = {}
if type is not None:
params['type'] = type
if category_id is not None:
params['category_id'] = category_id
return self.request(Route('GET', '/channels/{channel_id}/directory-entries', channel_id=channel_id), params=params)
def get_some_directory_entries(
self,
channel_id: Snowflake,
entity_ids: Sequence[Snowflake],
) -> Response[List[directory.PartialDirectoryEntry]]:
params = {'entity_ids': entity_ids}
return self.request(
Route('GET', '/channels/{channel_id}/directory-entries/list', channel_id=channel_id), params=params
)
def get_directory_counts(self, channel_id: Snowflake) -> Response[directory.DirectoryCounts]:
return self.request(Route('GET', '/channels/{channel_id}/directory-entries/counts', channel_id=channel_id))
def search_directory_entries(
self,
channel_id: Snowflake,
query: str,
*,
type: Optional[directory.DirectoryEntryType] = None,
category_id: Optional[directory.DirectoryCategory] = None,
) -> Response[List[directory.DirectoryEntry]]:
params: Dict[str, Any] = {'query': query}
if type is not None:
params['type'] = type
if category_id is not None:
params['category_id'] = category_id
return self.request(
Route('GET', '/channels/{channel_id}/directory-entries/search', channel_id=channel_id), params=params
)
def create_directory_entry(
self,
channel_id: Snowflake,
entity_id: Snowflake,
type: directory.DirectoryEntryType = MISSING,
primary_category_id: directory.DirectoryCategory = MISSING,
description: Optional[str] = MISSING,
) -> Response[directory.DirectoryEntry]:
payload = {}
if type is not MISSING:
payload['type'] = type
if primary_category_id is not MISSING:
payload['primary_category_id'] = primary_category_id
if description is not MISSING:
payload['description'] = description
return self.request(
Route('POST', '/channels/{channel_id}/directory-entry/{entity_id}', channel_id=channel_id, entity_id=entity_id),
json=payload,
)
def edit_directory_entry(
self,
channel_id: Snowflake,
entity_id: Snowflake,
description: Optional[str] = MISSING,
primary_category_id: directory.DirectoryCategory = MISSING,
) -> Response[directory.DirectoryEntry]:
payload = {}
if description is not MISSING:
payload['description'] = description or ''
if primary_category_id is not MISSING:
payload['primary_category_id'] = primary_category_id
return self.request(
Route('PATCH', '/channels/{channel_id}/directory-entry/{entity_id}', channel_id=channel_id, entity_id=entity_id),
json=payload,
)
def delete_directory_entry(self, channel_id: Snowflake, entity_id: int) -> Response[None]:
return self.request(
Route('DELETE', '/channels/{channel_id}/directory-entry/{entity_id}', channel_id=channel_id, entity_id=entity_id)
)
def get_directory_broadcast_info(self, guild_id: Snowflake, type: int) -> Response[directory.DirectoryBroadcast]:
params = {'type': type}
return self.request(Route('GET', '/guilds/{guild_id}/directory-entries/broadcast', guild_id=guild_id), params=params)
# Thread management
def start_thread_with_message(
@ -4705,3 +4801,35 @@ class HTTPClient:
) -> Response[Union[experiment.ExperimentResponse, experiment.ExperimentResponseWithGuild]]:
params = {'with_guild_experiments': str(with_guild_experiments).lower()}
return self.request(Route('GET', '/experiments'), params=params, context_properties=ContextProperties.empty())
# Hubs
def hub_waitlist_signup(self, email: str, school: str) -> Response[hub.HubWaitlist]:
payload = {'email': email, 'school': school}
return self.request(Route('POST', '/hub-waitlist/signup'), json=payload)
def hub_lookup(
self,
email: str,
guild_id: Optional[Snowflake] = None,
*,
use_verification_code: bool = True,
allow_multiple_guilds: bool = True,
) -> Response[hub.EmailDomainLookup]:
payload = {
'email': email,
'use_verification_code': use_verification_code,
'allow_multiple_guilds': allow_multiple_guilds,
}
if guild_id is not None:
payload['guild_id'] = guild_id
return self.request(Route('POST', '/guilds/automations/email-domain-lookup'), json=payload)
def join_hub(self, email: str, guild_id: Snowflake, code: str) -> Response[hub.EmailDomainVerification]:
payload = {'email': email, 'guild_id': guild_id, 'code': code}
return self.request(Route('POST', '/guilds/automations/email-domain-lookup/verify-code'), json=payload)
def join_hub_token(self, token: str) -> Response[hub.EmailDomainVerification]:
payload = {'token': token}
return self.request(Route('POST', '/guilds/automations/email-domain-lookup/verify'), json=payload)

32
discord/scheduled_event.py

@ -321,6 +321,7 @@ class ScheduledEvent(Hashable):
privacy_level: PrivacyLevel = ...,
status: EventStatus = ...,
image: bytes = ...,
directory_broadcast: bool = ...,
reason: Optional[str] = ...,
) -> ScheduledEvent:
...
@ -338,6 +339,7 @@ class ScheduledEvent(Hashable):
entity_type: Literal[EntityType.voice, EntityType.stage_instance],
status: EventStatus = ...,
image: bytes = ...,
directory_broadcast: bool = ...,
reason: Optional[str] = ...,
) -> ScheduledEvent:
...
@ -355,6 +357,7 @@ class ScheduledEvent(Hashable):
status: EventStatus = ...,
image: bytes = ...,
location: str,
directory_broadcast: bool = ...,
reason: Optional[str] = ...,
) -> ScheduledEvent:
...
@ -371,6 +374,7 @@ class ScheduledEvent(Hashable):
privacy_level: PrivacyLevel = ...,
status: EventStatus = ...,
image: bytes = ...,
directory_broadcast: bool = ...,
reason: Optional[str] = ...,
) -> ScheduledEvent:
...
@ -387,6 +391,7 @@ class ScheduledEvent(Hashable):
status: EventStatus = ...,
image: bytes = ...,
location: str,
directory_broadcast: bool = ...,
reason: Optional[str] = ...,
) -> ScheduledEvent:
...
@ -404,6 +409,7 @@ class ScheduledEvent(Hashable):
status: EventStatus = MISSING,
image: bytes = MISSING,
location: str = MISSING,
directory_broadcast: bool = MISSING,
reason: Optional[str] = None,
) -> ScheduledEvent:
r"""|coro|
@ -451,6 +457,11 @@ class ScheduledEvent(Hashable):
The new location of the scheduled event.
Required if the entity type is :attr:`EntityType.external`.
directory_broadcast: :class:`bool`
Whether to broadcast the scheduled event to the directories the guild is in.
You should first check eligibility with :meth:`Guild.directory_broadcast_eligibility`.
.. versionadded:: 2.1
reason: Optional[:class:`str`]
The reason for editing the scheduled event. Shows up on the audit log.
@ -492,7 +503,7 @@ class ScheduledEvent(Hashable):
if privacy_level is not MISSING:
if not isinstance(privacy_level, PrivacyLevel):
raise TypeError('privacy_level must be of type PrivacyLevel.')
raise TypeError('privacy_level must be of type PrivacyLevel')
payload['privacy_level'] = privacy_level.value
@ -523,7 +534,7 @@ class ScheduledEvent(Hashable):
if entity_type is None:
raise TypeError(
f'invalid GuildChannel type passed, must be VoiceChannel or StageChannel not {channel.__class__.__name__}'
f'invalid GuildChannel type passed; must be VoiceChannel or StageChannel not {channel.__class__.__name__}'
)
_entity_type = entity_type or self.entity_type
@ -563,6 +574,9 @@ class ScheduledEvent(Hashable):
else:
payload['scheduled_end_time'] = end_time
if directory_broadcast is not MISSING:
payload['broadcast_to_directory_channels'] = directory_broadcast
if metadata:
payload['entity_metadata'] = metadata
@ -680,37 +694,39 @@ class ScheduledEvent(Hashable):
count = 0
for count, user in enumerate(users, 1):
if user.id not in self._users:
self._add_user(user)
yield user
if count < 100:
# There's no data left after this
break
async def subscribe(self) -> None:
async def rsvp(self) -> None:
"""|coro|
Subscribes the current user to this event.
Marks the current user as interested in this event.
.. versionadded:: 2.1
Raises
-------
HTTPException
Subscribing failed.
RSVPing failed.
"""
await self._state.http.create_scheduled_event_user(self.guild_id, self.id)
async def unsubscribe(self) -> None:
async def unrsvp(self) -> None:
"""|coro|
Unsubscribes the current user from this event.
Unmarks the current user as interested in this event.
.. versionadded:: 2.1
Raises
-------
HTTPException
Unsubscribing failed.
Un-RSVPing failed.
"""
await self._state.http.delete_scheduled_event_user(self.guild_id, self.id)

25
discord/types/channel.py

@ -40,21 +40,21 @@ class PermissionOverwrite(TypedDict):
deny: str
ChannelTypeWithoutThread = Literal[0, 1, 2, 3, 4, 5, 6, 13, 15]
ChannelTypeWithoutThread = Literal[0, 1, 2, 3, 4, 5, 6, 13, 14, 15]
ChannelType = Union[ChannelTypeWithoutThread, ThreadType]
class _BaseChannel(TypedDict):
id: Snowflake
name: str
class _BaseGuildChannel(_BaseChannel):
guild_id: Snowflake
position: int
permission_overwrites: List[PermissionOverwrite]
nsfw: bool
parent_id: Optional[Snowflake]
name: str
flags: int
class PartialRecipient(TypedDict):
@ -62,6 +62,7 @@ class PartialRecipient(TypedDict):
class PartialChannel(_BaseChannel):
name: Optional[str]
type: ChannelType
icon: NotRequired[Optional[str]]
recipients: NotRequired[List[PartialRecipient]]
@ -74,6 +75,7 @@ class _BaseTextChannel(_BaseGuildChannel, total=False):
rate_limit_per_user: int
default_thread_rate_limit_per_user: int
default_auto_archive_duration: ThreadArchiveDuration
nsfw: bool
class TextChannel(_BaseTextChannel):
@ -105,6 +107,13 @@ class StageChannel(_BaseGuildChannel):
user_limit: int
rtc_region: NotRequired[Optional[str]]
topic: NotRequired[str]
nsfw: bool
class DirectoryChannel(_BaseGuildChannel):
type: Literal[14]
last_message_id: Optional[Snowflake]
topic: Optional[str]
class ThreadChannel(_BaseChannel):
@ -123,8 +132,8 @@ class ThreadChannel(_BaseChannel):
rate_limit_per_user: NotRequired[int]
last_message_id: NotRequired[Optional[Snowflake]]
last_pin_timestamp: NotRequired[str]
flags: NotRequired[int]
applied_tags: NotRequired[List[Snowflake]]
flags: int
class DefaultReaction(TypedDict):
@ -150,10 +159,11 @@ class ForumChannel(_BaseTextChannel):
default_reaction_emoji: Optional[DefaultReaction]
default_sort_order: Optional[ForumOrderType]
default_forum_layout: NotRequired[ForumLayoutType]
flags: NotRequired[int]
GuildChannel = Union[TextChannel, NewsChannel, VoiceChannel, CategoryChannel, StageChannel, ThreadChannel, ForumChannel]
GuildChannel = Union[
TextChannel, NewsChannel, VoiceChannel, CategoryChannel, StageChannel, DirectoryChannel, ThreadChannel, ForumChannel
]
class DMChannel(_BaseChannel):
@ -167,6 +177,7 @@ class DMChannel(_BaseChannel):
class GroupDMChannel(_BaseChannel):
type: Literal[3]
name: Optional[str]
icon: Optional[str]
owner_id: Snowflake
recipients: List[PartialUser]
@ -174,7 +185,7 @@ class GroupDMChannel(_BaseChannel):
Channel = Union[GuildChannel, DMChannel, GroupDMChannel]
PrivacyLevel = Literal[2]
PrivacyLevel = Literal[1, 2]
class StageInstance(TypedDict):

81
discord/types/directory.py

@ -0,0 +1,81 @@
"""
The MIT License (MIT)
Copyright (c) 2021-present Dolfies
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 typing import Dict, Literal, Optional, TypedDict, Union
from typing_extensions import NotRequired
from .guild import PartialGuild, _GuildCounts
from .scheduled_event import ExternalScheduledEvent, StageInstanceScheduledEvent, VoiceScheduledEvent
from .snowflake import Snowflake
class _DirectoryScheduledEvent(TypedDict):
guild: PartialGuild
user_rsvp: bool
user_count: int
class _DirectoryStageInstanceScheduledEvent(_DirectoryScheduledEvent, StageInstanceScheduledEvent):
...
class _DirectoryVoiceScheduledEvent(_DirectoryScheduledEvent, VoiceScheduledEvent):
...
class _DirectoryExternalScheduledEvent(_DirectoryScheduledEvent, ExternalScheduledEvent):
...
DirectoryScheduledEvent = Union[
_DirectoryStageInstanceScheduledEvent, _DirectoryVoiceScheduledEvent, _DirectoryExternalScheduledEvent
]
class DirectoryGuild(PartialGuild, _GuildCounts):
featurable_in_directory: bool
DirectoryEntryType = Literal[0, 1]
DirectoryCategory = Literal[0, 1, 2, 3, 5]
DirectoryCounts = Dict[DirectoryCategory, int]
class PartialDirectoryEntry(TypedDict):
type: DirectoryEntryType
primary_category_id: NotRequired[DirectoryCategory]
directory_channel_id: Snowflake
author_id: Snowflake
entity_id: Snowflake
created_at: str
description: Optional[str]
class DirectoryEntry(PartialDirectoryEntry):
guild: NotRequired[DirectoryGuild]
guild_scheduled_event: NotRequired[DirectoryScheduledEvent]
class DirectoryBroadcast(TypedDict):
can_broadcast: bool

18
discord/types/guild.py

@ -66,26 +66,27 @@ class BaseGuild(TypedDict):
class PartialGuild(BaseGuild):
name: str
icon: Optional[str]
description: Optional[str]
splash: Optional[str]
discovery_splash: Optional[str]
home_header: Optional[str]
class _GuildMedia(PartialGuild):
emojis: List[Emoji]
stickers: List[GuildSticker]
features: List[str]
description: Optional[str]
class _GuildPreviewUnique(TypedDict):
class _GuildCounts(TypedDict):
approximate_member_count: int
approximate_presence_count: int
class GuildPreview(PartialGuild, _GuildPreviewUnique):
class GuildPreview(_GuildMedia, _GuildCounts):
...
class Guild(UnavailableGuild, PartialGuild):
class Guild(UnavailableGuild, _GuildMedia):
owner_id: Snowflake
region: str
afk_channel_id: Optional[Snowflake]
@ -125,6 +126,7 @@ class Guild(UnavailableGuild, PartialGuild):
premium_subscription_count: NotRequired[int]
max_video_channel_users: NotRequired[int]
application_command_counts: ApplicationCommandCounts
hub_type: Optional[Literal[0, 1, 2]]
class UserGuild(BaseGuild):
@ -138,7 +140,7 @@ class InviteGuild(Guild, total=False):
welcome_screen: WelcomeScreen
class GuildWithCounts(Guild, _GuildPreviewUnique):
class GuildWithCounts(Guild, _GuildCounts):
...

52
discord/types/hub.py

@ -0,0 +1,52 @@
"""
The MIT License (MIT)
Copyright (c) 2021-present Dolfies
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 typing import List, Optional, TypedDict
from typing_extensions import NotRequired
from .guild import Guild
from .snowflake import Snowflake
class HubWaitlist(TypedDict):
email: str
email_domain: str
school: str
user_id: Snowflake
class HubGuild(TypedDict):
id: Snowflake
name: str
icon: Optional[str]
class EmailDomainLookup(TypedDict):
guilds_info: NotRequired[List[HubGuild]]
has_matching_guild: bool
class EmailDomainVerification(TypedDict):
guild: Guild
joined: bool

4
discord/types/invite.py

@ -29,7 +29,7 @@ from typing_extensions import NotRequired
from .scheduled_event import GuildScheduledEvent
from .snowflake import Snowflake
from .guild import InviteGuild, _GuildPreviewUnique
from .guild import InviteGuild, _GuildCounts
from .channel import PartialChannel
from .user import PartialUser
from .application import PartialApplication
@ -65,7 +65,7 @@ class Invite(IncompleteInvite, total=False):
guild_scheduled_event: GuildScheduledEvent
class InviteWithCounts(Invite, _GuildPreviewUnique):
class InviteWithCounts(Invite, _GuildCounts):
...

1
discord/types/scheduled_event.py

@ -47,6 +47,7 @@ class _BaseGuildScheduledEvent(TypedDict):
creator: NotRequired[User]
user_count: NotRequired[int]
image: NotRequired[Optional[str]]
sku_ids: List[Snowflake]
class _VoiceChannelScheduledEvent(_BaseGuildScheduledEvent):

5
discord/types/webhook.py

@ -38,6 +38,11 @@ class SourceGuild(TypedDict):
icon: str
class SourceChannel(TypedDict):
id: int
name: str
WebhookType = Literal[1, 2, 3]

6
discord/webhook/async_.py

@ -75,6 +75,7 @@ if TYPE_CHECKING:
from ..types.webhook import (
Webhook as WebhookPayload,
SourceGuild as SourceGuildPayload,
SourceChannel as SourceChannelPayload,
)
from ..types.message import (
Message as MessagePayload,
@ -83,9 +84,6 @@ if TYPE_CHECKING:
User as UserPayload,
PartialUser as PartialUserPayload,
)
from ..types.channel import (
PartialChannel as PartialChannelPayload,
)
from ..types.emoji import PartialEmoji as PartialEmojiPayload
BE = TypeVar('BE', bound=BaseException)
@ -443,7 +441,7 @@ class PartialWebhookChannel(Hashable):
__slots__ = ('id', 'name')
def __init__(self, *, data: PartialChannelPayload) -> None:
def __init__(self, *, data: SourceChannelPayload) -> None:
self.id: int = int(data['id'])
self.name: str = data['name']

93
docs/api.rst

@ -1691,22 +1691,28 @@ of :class:`enum.Enum`.
.. attribute:: news_thread
A news thread
A news thread.
.. versionadded:: 2.0
.. attribute:: public_thread
A public thread
A public thread.
.. versionadded:: 2.0
.. attribute:: private_thread
A private thread
A private thread.
.. versionadded:: 2.0
.. attribute:: directory
A directory channel.
.. versionadded:: 2.1
.. attribute:: forum
A forum channel.
@ -3559,7 +3565,7 @@ of :class:`enum.Enum`.
.. attribute:: canceled
Alias for :attr:`cancelled`.
An alias for :attr:`cancelled`.
.. attribute:: deferred
@ -4185,7 +4191,7 @@ of :class:`enum.Enum`.
.. attribute:: canceled
Alias for :attr:`PaymentStatus.cancelled`.
An alias for :attr:`PaymentStatus.cancelled`.
.. class:: EntitlementType
@ -5730,6 +5736,67 @@ of :class:`enum.Enum`.
Represents a guild-bound read state for guild onboarding. Only one exists per guild.
.. class:: DirectoryEntryType
Represents the type of a directory entry.
.. versionadded:: 2.1
.. attribute:: guild
Represents a guild directory entry.
.. attribute:: scheduled_event
Represents a broadcasted scheduled event directory entry.
.. class:: DirectoryCategory
Represents the category of a directory entry.
.. versionadded:: 2.1
.. attribute:: uncategorized
The directory entry is uncategorized.
.. attribute:: school_club
The directory entry is a school club.
.. attribute:: class_subject
The directory entry is a class/subject.
.. attribute:: study_social
The directory entry is a study/social venue.
.. attribute:: miscellaneous
The directory entry is miscellaneous.
.. class:: HubType
Represents the type of Student Hub a guild is.
.. versionadded:: 2.1
.. attribute:: default
The Student Hub is not categorized as a high school or post-secondary institution.
.. attribute:: high_school
The Student Hub is for a high school.
.. attribute:: college
The Student Hub is for a post-secondary institution (college or university).
.. attribute:: university
An alias for :attr:`college`.
.. _discord-api-audit-logs:
@ -7345,6 +7412,12 @@ GuildChannel
:members:
:inherited-members:
.. attributetable:: DirectoryChannel
.. autoclass:: DirectoryChannel()
:members:
:inherited-members:
.. attributetable:: ForumChannel
.. autoclass:: ForumChannel()
@ -7832,12 +7905,20 @@ Permissions
.. autoclass:: PermissionOverwrite()
:members:
DirectoryEntry
~~~~~~~~~~~~~~~
.. attributetable:: DirectoryEntry
.. autoclass:: DirectoryEntry()
:members:
ForumTag
~~~~~~~~~
.. attributetable:: ForumTag
.. autoclass:: ForumTag
.. autoclass:: ForumTag()
:members:
Experiment

Loading…
Cancel
Save