diff --git a/discord/enums.py b/discord/enums.py index de5ea6a11..7f32cc007 100644 --- a/discord/enums.py +++ b/discord/enums.py @@ -69,6 +69,7 @@ __all__ = ( 'ForumLayoutType', 'ForumOrderType', 'OnboardingPromptType', + 'OnboardingMode', ) if TYPE_CHECKING: @@ -776,6 +777,11 @@ class OnboardingPromptType(Enum): dropdown = 1 +class OnboardingMode(Enum): + default = 0 + advanced = 1 + + 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}' diff --git a/discord/guild.py b/discord/guild.py index 87c97b488..60e8c9cd0 100644 --- a/discord/guild.py +++ b/discord/guild.py @@ -75,6 +75,7 @@ from .enums import ( AutoModRuleEventType, ForumOrderType, ForumLayoutType, + OnboardingMode, ) from .mixins import Hashable from .user import User @@ -90,7 +91,7 @@ from .sticker import GuildSticker from .file import File from .audit_logs import AuditLogEntry from .object import OLDEST_OBJECT, Object -from .onboarding import Onboarding +from .onboarding import Onboarding, PartialOnboardingPrompt from .welcome_screen import WelcomeScreen, WelcomeChannel from .automod import AutoModRule, AutoModTrigger, AutoModRuleAction from .partial_emoji import _EmojiTag, PartialEmoji @@ -4293,3 +4294,53 @@ class Guild(Hashable): """ data = await self._state.http.get_guild_onboarding(self.id) return Onboarding(data=data, guild=self, state=self._state) + + async def edit_onboarding( + self, + *, + prompts: List[PartialOnboardingPrompt] = MISSING, + default_channels: List[Snowflake] = MISSING, + enabled: bool = MISSING, + mode: OnboardingMode = MISSING, + reason: str = MISSING, + ) -> Onboarding: + """|coro| + + Edits the onboarding configuration for this guild. + + .. versionadded:: 2.4 + + Parameters + ----------- + prompts: List[:class:`PartialOnboardingPrompt`] + The prompts that will be shown to new members. + default_channels: List[:class:`abc.Snowflake`] + The channels that will be used as the default channels for new members. + enabled: :class:`bool` + Whether the onboarding configuration is enabled. + mode: :class:`OnboardingMode` + The mode that will be used for the onboarding configuration. + reason: :class:`str` + The reason for editing the onboarding configuration. Shows up on the audit log. + + Raises + ------- + Forbidden + You do not have permissions to edit the onboarding configuration. + HTTPException + Editing the onboarding configuration failed. + + Returns + -------- + :class:`Onboarding` + The new onboarding configuration. + """ + data = await self._state.http.modify_guild_onboarding( + self.id, + prompts=[p.to_dict() for p in prompts] if prompts is not MISSING else None, + default_channel_ids=[c.id for c in default_channels] if default_channels is not MISSING else None, + enabled=enabled if enabled is not MISSING else None, + mode=mode.value if mode is not MISSING else None, + reason=reason if reason is not MISSING else None, + ) + return Onboarding(data=data, guild=self, state=self._state) diff --git a/discord/http.py b/discord/http.py index d30d7a14b..2c136acaa 100644 --- a/discord/http.py +++ b/discord/http.py @@ -2379,6 +2379,37 @@ class HTTPClient: def get_guild_onboarding(self, guild_id: Snowflake) -> Response[onboarding.Onboarding]: return self.request(Route('GET', '/guilds/{guild_id}/onboarding', guild_id=guild_id)) + def modify_guild_onboarding( + self, + guild_id: Snowflake, + *, + prompts: Optional[List[onboarding.Prompt]] = None, + default_channel_ids: Optional[List[Snowflake]] = None, + enabled: Optional[bool] = None, + mode: Optional[onboarding.OnboardingMode] = None, + reason: Optional[str], + ) -> Response[onboarding.Onboarding]: + + payload = {} + + if prompts is not None: + payload['prompts'] = prompts + + if default_channel_ids is not None: + payload['default_channel_ids'] = default_channel_ids + + if enabled is not None: + payload['enabled'] = enabled + + if mode is not None: + payload['mode'] = mode + + return self.request( + Route('PUT', f'/guilds/{guild_id}/onboarding', guild_id=guild_id), + json=payload, + reason=reason, + ) + # Misc def application_info(self) -> Response[appinfo.AppInfo]: diff --git a/discord/onboarding.py b/discord/onboarding.py index e400cd6c6..b88b21d9b 100644 --- a/discord/onboarding.py +++ b/discord/onboarding.py @@ -24,16 +24,24 @@ DEALINGS IN THE SOFTWARE. from __future__ import annotations -from typing import TYPE_CHECKING, Optional, Set, List, Union +import os +from typing import TYPE_CHECKING, Iterable, Optional, Set, List, Union + +from discord.types.onboarding import Prompt as PromptPayload +from discord.utils import MISSING from . import utils -from .enums import OnboardingPromptType, try_enum -from .utils import cached_slot_property +from .mixins import Hashable +from .enums import OnboardingMode, OnboardingPromptType, try_enum +from .state import ConnectionState +from .utils import cached_slot_property, MISSING __all__ = ( 'Onboarding', + 'PartialOnboardingPrompt', 'OnboardingPrompt', 'OnboardingPromptOption', + 'PartialOnboardingPromptOption', ) @@ -43,7 +51,6 @@ if TYPE_CHECKING: from .guild import Guild from .partial_emoji import PartialEmoji from .role import Role - from .state import ConnectionState from .threads import Thread from .types.onboarding import ( Prompt as PromptPayload, @@ -52,7 +59,60 @@ if TYPE_CHECKING: ) -class OnboardingPromptOption: +class PartialOnboardingPromptOption: + """Represents a partial onboarding prompt option, these are used in the creation + of an :class:`OnboardingPrompt` via :meth:`Guild.edit_onboarding`. + + .. versionadded:: 2.4 + + Attributes + ----------- + title: :class:`str` + The title of this prompt option. + description: Optional[:class:`str`] + The description of this prompt option. + emoji: Optional[Union[:class:`Emoji`, :class:`PartialEmoji`, :class:`str`]] + The emoji tied to this option. May be a custom emoji, or a unicode emoji. + channel_ids: Set[:class:`int`] + The IDs of the channels that will be made visible if this option is selected. + role_ids: Set[:class:`int`] + The IDs of the roles given to the user if this option is selected. + """ + + __slots__ = ( + 'title', + 'emoji', + 'description', + 'channel_ids', + 'role_ids', + ) + + def __init__( + self, + title: str, + emoji: Union[Emoji, PartialEmoji, str], + description: Optional[str] = None, + channel_ids: Iterable[int] = MISSING, + role_ids: Iterable[int] = MISSING, + ) -> None: + self.title: str = title + self.description: Optional[str] = description + self.emoji: Union[PartialEmoji, Emoji, str] = emoji + self.channel_ids: Set[int] = set(channel_ids or []) + self.role_ids: Set[int] = set(role_ids or []) + + def to_dict(self, *, id: int = MISSING) -> PromptOptionPayload: + return { + 'id': id or os.urandom(16).hex(), + 'title': self.title, + 'description': self.description, + 'emoji': ConnectionState.emoji_to_partial_payload(self.emoji), + 'channel_ids': list(self.channel_ids), + 'role_ids': list(self.role_ids), + } + + +class OnboardingPromptOption(PartialOnboardingPromptOption, Hashable): """Represents an onboarding prompt option. .. versionadded:: 2.4 @@ -81,22 +141,19 @@ class OnboardingPromptOption: '_cs_roles', 'guild', 'id', - 'title', - 'description', - 'emoji', - 'channel_ids', - 'role_ids', ) def __init__(self, *, data: PromptOptionPayload, state: ConnectionState, guild: Guild) -> None: self._state: ConnectionState = state self.guild: Guild = guild self.id: int = int(data['id']) - self.title: str = data['title'] - self.description: Optional[str] = data['description'] - self.emoji: Optional[Union[PartialEmoji, Emoji, str]] = self._state.get_emoji_from_partial_payload(data['emoji']) - self.channel_ids: Set[int] = {int(channel_id) for channel_id in data['channel_ids']} - self.role_ids: Set[int] = {int(role_id) for role_id in data['role_ids']} + super().__init__( + title=data['title'], + description=data['description'], + emoji=self._state.get_emoji_from_partial_payload(data['emoji']), + channel_ids=[int(id) for id in data['channel_ids']], + role_ids=[int(id) for id in data['role_ids']], + ) def __repr__(self) -> str: return f'' @@ -113,8 +170,71 @@ class OnboardingPromptOption: it = filter(None, map(self.guild.get_role, self.role_ids)) return utils._unique(it) + def to_dict(self) -> PromptOptionPayload: + return super().to_dict(id=self.id) + + +class PartialOnboardingPrompt: + """Represents a partial onboarding prompt, these are used in the creation + of an :class:`Onboarding` via :meth:`Guild.edit_onboarding`. + + .. versionadded:: 2.4 + + Attributes + ----------- + type: :class:`OnboardingPromptType` + The type of this prompt. + title: :class:`str` + The title of this prompt. + options: List[:class:`PartialOnboardingPromptOption`] + The options of this prompt. + single_select: :class:`bool` + Whether this prompt is single select. + required: :class:`bool` + Whether this prompt is required. + in_onboarding: :class:`bool` + Whether this prompt is in the onboarding flow. + """ + + __slots__ = ( + 'type', + 'title', + 'options', + 'single_select', + 'required', + 'in_onboarding', + ) -class OnboardingPrompt: + def __init__( + self, + *, + type: OnboardingPromptType, + title: str, + options: List[PartialOnboardingPromptOption], + single_select: bool = True, + required: bool = True, + in_onboarding: bool = True, + ) -> None: + self.type: OnboardingPromptType = try_enum(OnboardingPromptType, type) + self.title: str = title + self.options: List[PartialOnboardingPromptOption] = options + self.single_select: bool = single_select + self.required: bool = required + self.in_onboarding: bool = in_onboarding + + def to_dict(self, *, id: int = MISSING) -> PromptPayload: + return { + 'id': id or os.urandom(16).hex(), + 'type': self.type.value, + 'title': self.title, + 'options': [option.to_dict() for option in self.options], + 'single_select': self.single_select, + 'required': self.required, + 'in_onboarding': self.in_onboarding, + } + + +class OnboardingPrompt(PartialOnboardingPrompt, Hashable): """Represents an onboarding prompt. .. versionadded:: 2.4 @@ -139,6 +259,8 @@ class OnboardingPrompt: Whether this prompt is part of the onboarding flow. """ + options: List[OnboardingPromptOption] + __slots__ = ( '_state', 'guild', @@ -155,18 +277,21 @@ class OnboardingPrompt: self._state: ConnectionState = state self.guild: Guild = guild self.id: int = int(data['id']) - self.title: str = data['title'] - self.options: List[OnboardingPromptOption] = [ - OnboardingPromptOption(data=option_data, state=state, guild=guild) for option_data in data['options'] - ] - self.single_select: bool = data['single_select'] - self.required: bool = data['required'] - self.in_onboarding: bool = data['in_onboarding'] - self.type: OnboardingPromptType = try_enum(OnboardingPromptType, data['type']) + super().__init__( + type=try_enum(OnboardingPromptType, data['type']), + title=data['title'], + options=[OnboardingPromptOption(data=option_data, state=state, guild=guild) for option_data in data['options']], + single_select=data['single_select'], + required=data['required'], + in_onboarding=data['in_onboarding'], + ) def __repr__(self) -> str: return f'' + def to_dict(self) -> PromptPayload: + return super().to_dict(id=self.id) + class Onboarding: """Represents a guild's onboarding configuration. @@ -183,6 +308,8 @@ class Onboarding: The IDs of the channels exposed to a new user by default. enabled: :class:`bool`: Whether onboarding is enabled in this guild. + mode: :class:`OnboardingMode` + The mode of onboarding for this guild. """ __slots__ = ( @@ -192,6 +319,7 @@ class Onboarding: 'prompts', 'default_channel_ids', 'enabled', + 'mode', ) def __init__(self, *, data: OnboardingPayload, guild: Guild, state: ConnectionState) -> None: @@ -202,6 +330,7 @@ class Onboarding: OnboardingPrompt(data=prompt_data, state=state, guild=guild) for prompt_data in data['prompts'] ] self.enabled: bool = data['enabled'] + self.mode: OnboardingMode = try_enum(OnboardingMode, data.get('mode', 0)) def __repr__(self) -> str: return f'' diff --git a/discord/state.py b/discord/state.py index 0d88c6f49..a9afd07fb 100644 --- a/discord/state.py +++ b/discord/state.py @@ -1587,6 +1587,14 @@ class ConnectionState(Generic[ClientT]): self, animated=data.get('animated', False), id=emoji_id, name=data['name'] # type: ignore ) + @staticmethod + def emoji_to_partial_payload(emoji: Union[Emoji, PartialEmoji, str]) -> PartialEmojiPayload: + if isinstance(emoji, str): + return {'name': emoji} # type: ignore + elif isinstance(emoji, Emoji): + emoji = emoji._to_partial() + return emoji.to_dict() + def _upgrade_partial_emoji(self, emoji: PartialEmoji) -> Union[Emoji, PartialEmoji, str]: emoji_id = emoji.id if not emoji_id: diff --git a/discord/types/onboarding.py b/discord/types/onboarding.py index 11a85c8de..64fa199c4 100644 --- a/discord/types/onboarding.py +++ b/discord/types/onboarding.py @@ -31,6 +31,7 @@ from .snowflake import Snowflake PromptType = Literal[0, 1] +OnboardingMode = Literal[0, 1] class PromptOption(TypedDict): @@ -57,3 +58,4 @@ class Onboarding(TypedDict): prompts: list[Prompt] default_channel_ids: list[Snowflake] enabled: bool + mode: OnboardingMode diff --git a/docs/api.rst b/docs/api.rst index a5ec43a57..3f0e00a93 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -3388,6 +3388,20 @@ of :class:`enum.Enum`. Prompt options are displayed as a drop-down. +.. class:: OnboardingMode + + Represents the onboarding constriant mode. + + .. versionadded:: 2.4 + + .. attribute:: default + + Only default channels count towards onboarding constraints. + + .. attribute:: advanced + + Default channels and questions count towards onboarding constraints. + .. _discord-api-audit-logs: @@ -4730,6 +4744,14 @@ Onboarding .. autoclass:: Onboarding() :members: +PartialOnboardingPrompt +~~~~~~~~~~~~~~~~~~~~~~~~ + +.. attributetable:: PartialOnboardingPrompt + +.. autoclass:: PartialOnboardingPrompt + :members: + OnboardingPrompt ~~~~~~~~~~~~~~~~~ @@ -4738,6 +4760,14 @@ OnboardingPrompt .. autoclass:: OnboardingPrompt() :members: +PartialOnboardingPromptOption +~~~~~~~~~~~~~~~~~~~~~~~~ + +.. attributetable:: PartialOnboardingPromptOption + +.. autoclass:: PartialOnboardingPromptOption + :members: + OnboardingPromptOption ~~~~~~~~~~~~~~~~~~~~~~~