From 20f14b817433f1ac87401ad7db9c32ed76f336e6 Mon Sep 17 00:00:00 2001 From: Josh <8677174+bijij@users.noreply.github.com> Date: Sat, 25 Feb 2023 16:02:04 +1000 Subject: [PATCH 01/20] Begin implementaton of guild onboarding configuration support --- discord/__init__.py | 1 + discord/enums.py | 6 ++ discord/guild.py | 17 +++ discord/http.py | 4 + discord/onboarding.py | 209 ++++++++++++++++++++++++++++++++++++ discord/types/onboarding.py | 59 ++++++++++ docs/api.rst | 38 +++++++ 7 files changed, 334 insertions(+) create mode 100644 discord/onboarding.py create mode 100644 discord/types/onboarding.py diff --git a/discord/__init__.py b/discord/__init__.py index 014615354..51de8d09c 100644 --- a/discord/__init__.py +++ b/discord/__init__.py @@ -68,6 +68,7 @@ from .interactions import * from .components import * from .threads import * from .automod import * +from .onboarding import * class VersionInfo(NamedTuple): diff --git a/discord/enums.py b/discord/enums.py index 43ae5b4f7..8fcadf1eb 100644 --- a/discord/enums.py +++ b/discord/enums.py @@ -67,6 +67,7 @@ __all__ = ( 'AutoModRuleEventType', 'AutoModRuleActionType', 'ForumLayoutType', + 'OnboardingPromptType', ) if TYPE_CHECKING: @@ -751,6 +752,11 @@ class ForumLayoutType(Enum): gallery_view = 2 +class OnboardingPromptType(Enum): + multiple_choice = 0 + dropdown = 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 078a7b8be..ce8324b99 100644 --- a/discord/guild.py +++ b/discord/guild.py @@ -88,6 +88,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 .welcome_screen import WelcomeScreen, WelcomeChannel from .automod import AutoModRule, AutoModTrigger, AutoModRuleAction @@ -4026,3 +4027,19 @@ class Guild(Hashable): ) return AutoModRule(data=data, guild=self, state=self._state) + + async def fetch_onboarding(self) -> Onboarding: + """|coro| + + Fetches the onboarding configuration for this guild. + + .. versionadded:: 2.2 + + Returns + -------- + :class:`Onboarding` + The onboarding configuration that was fetched. + """ + data = await self._state.http.get_guild_onboarding(self.id) + + return Onboarding(data=data, guild=self, state=self._state) \ No newline at end of file diff --git a/discord/http.py b/discord/http.py index 5913f4911..70ad63191 100644 --- a/discord/http.py +++ b/discord/http.py @@ -81,6 +81,7 @@ if TYPE_CHECKING: invite, member, message, + onboarding, template, role, user, @@ -2359,6 +2360,9 @@ class HTTPClient: reason=reason, ) + def get_guild_onboarding(self, guild_id: Snowflake) -> Response[onboarding.Onboarding]: + return self.request(Route('GET', '/guilds/{guild_id}/onboarding', guild_id=guild_id)) + # Misc def application_info(self) -> Response[appinfo.AppInfo]: diff --git a/discord/onboarding.py b/discord/onboarding.py new file mode 100644 index 000000000..c806541c6 --- /dev/null +++ b/discord/onboarding.py @@ -0,0 +1,209 @@ +""" +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 TYPE_CHECKING, Optional, Set, List, Union + +from . import utils +from .enums import OnboardingPromptType, try_enum +from .utils import cached_slot_property + +__all__ = ('Onboarding', 'OnboardingPrompt', 'OnboardingPromptOption') + + +if TYPE_CHECKING: + from .abc import GuildChannel + from .emoji import Emoji + 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, + PromptOption as PromptOptionPayload, + Onboarding as OnboardingPayload, + ) + + +class OnboardingPromptOption: + """Represents an onboarding prompt option. + + .. versionadded:: 2.2 + + Attributes + ----------- + id: :class:`int` + The ID of this prompt option. + guild: :class:`Guild` + The guild the onboarding prompt option is related to. + title: :class:`str` + The title of this prompt option. + description: Optional[:class:`str`] + The description of this prompt option. + emoji: 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 set 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__ = ( + '_state', + '_cs_channels', + '_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: Union[PartialEmoji, Emoji, str] = self._state.get_reaction_emoji(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']} + + def __repr__(self) -> str: + return f'' + + @cached_slot_property('_cs_channels') + def channels(self) -> List[Union[GuildChannel, Thread]]: + """List[Union[:class:`abc.GuildChannel`, :class:`Thread`]]: The list of channels which will be set visible if this option is selected.""" + it = filter(None, map(self.guild._resolve_channel, self.channel_ids)) + return utils._unique(it) + + @cached_slot_property('_cs_roles') + def default_channels(self) -> List[Role]: + """List[Union[:class:`abc.GuildChannel`, :class:`Thread`]]: The list of roles given to the user if this option is selected.""" + it = filter(None, map(self.guild.get_role, self.role_ids)) + return utils._unique(it) + + +class OnboardingPrompt: + """Represents an onboarding prompt. + + .. versionadded:: 2.2 + + Attributes + ----------- + id: :class:`int` + The ID of this prompt. + guild: :class:`Guild` + The guild the onboarding prompt is related to. + type: :class:`OnboardingPromptType` + The type of onboarding prompt. + title: :class:`str` + The title of this prompt. + options: List[:class:`OnboardingPromptOption`] + The list of options the user can select from. + single_select: :class:`bool` + Whether only one option can be selected. + required: :class:`bool` + Whether this prompt is required to complete the onboarding flow. + in_onboarding: :class:`bool` + Whether this prompt is part of the onboarding flow. + """ + + __slots__ = ( + '_state', + 'guild', + 'id', + 'title', + 'options', + 'single_select', + 'required', + 'in_onboarding', + 'type', + ) + + def __init__(self, *, data: PromptPayload, state: ConnectionState, guild: Guild): + 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']) + + def __repr__(self) -> str: + return f'' + + +class Onboarding: + """Represents a guild's onboarding configuration. + + .. versionadded:: 2.2 + + Attributes + ----------- + guild: :class:`Guild` + The guild the onboarding configuration is for. + prompts: List[:class:`OnboardingPrompt`] + The list of prompts shown during the onboarding and customize community flows. + default_channel_ids: Set[:class:`int`] + The IDs of the channels exposed to a new user by default. + enabled: :class:`bool`: + Whether onboarding is enabled in this guild. + """ + + __slots__ = ( + '_state', + '_cs_default_channels', + 'guild', + 'prompts', + 'default_channel_ids', + 'enabled', + ) + + def __init__(self, *, data: OnboardingPayload, guild: Guild, state: ConnectionState) -> None: + self._state: ConnectionState = state + self.guild: Guild = guild + self.default_channel_ids: Set[int] = {int(channel_id) for channel_id in data['default_channel_ids']} + self.prompts: List[OnboardingPrompt] = [ + OnboardingPrompt(data=prompt_data, state=state, guild=guild) for prompt_data in data['prompts'] + ] + self.enabled: bool = data['enabled'] + + def __repr__(self) -> str: + return f'' + + @cached_slot_property('_cs_default_channels') + def default_channels(self) -> List[Union[GuildChannel, Thread]]: + """List[Union[:class:`abc.GuildChannel`, :class:`Thread`]]: The list of channels exposed to a new user by default.""" + it = filter(None, map(self.guild._resolve_channel, self.default_channel_ids)) + return utils._unique(it) diff --git a/discord/types/onboarding.py b/discord/types/onboarding.py new file mode 100644 index 000000000..ab90f8533 --- /dev/null +++ b/discord/types/onboarding.py @@ -0,0 +1,59 @@ +""" +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 Literal, Optional, TypedDict + +from .emoji import Emoji +from .snowflake import Snowflake + + +PromptType = Literal[0, 1] + + +class PromptOption(TypedDict): + id: Snowflake + channel_ids: list[Snowflake] + role_ids: list[Snowflake] + emoji: Emoji + title: str + description: Optional[str] + + +class Prompt(TypedDict): + id: Snowflake + options: list[PromptOption] + title: str + single_select: bool + required: bool + in_onboarding: bool + type: PromptType + + +class Onboarding(TypedDict): + guild_id: Snowflake + prompts: list[Prompt] + default_channel_ids: list[Snowflake] + enabled: bool diff --git a/docs/api.rst b/docs/api.rst index 32e393ef2..4f64a0c9d 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -3276,6 +3276,20 @@ of :class:`enum.Enum`. Displays posts as a collection of tiles. +.. class:: OnboardingPromptType + + Represents the type of onboarding prompt. + + .. versionadded:: 2.2 + + .. attribute:: multiple_choice + + Prompt options are multiple choice. + + .. attribute:: dropdown + + Prompt options are displayed as a drop-down. + .. _discord-api-audit-logs: @@ -4480,6 +4494,30 @@ GuildSticker .. autoclass:: GuildSticker() :members: +Onboarding +~~~~~~~~~~~ + +.. attributetable:: Onboarding + +.. autoclass:: Onboarding() + :members: + +OnboardingPrompt +~~~~~~~~~~~~~~~~~ + +.. attributetable:: OnboardingPrompt + +.. autoclass:: OnboardingPrompt() + :members: + +OnboardingPromptOption +~~~~~~~~~~~~~~~~~~~~~~~ + +.. attributetable:: OnboardingPromptOption + +.. autoclass:: OnboardingPromptOption() + :members: + ShardInfo ~~~~~~~~~~~ From 3fd0c25c88d9f54faa63edb1f5611f689c8fa26a Mon Sep 17 00:00:00 2001 From: Josh <8677174+bijij@users.noreply.github.com> Date: Sat, 25 Feb 2023 18:23:44 +1000 Subject: [PATCH 02/20] Add missing newline at eof in guild.py --- discord/guild.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/discord/guild.py b/discord/guild.py index ce8324b99..32a7416ec 100644 --- a/discord/guild.py +++ b/discord/guild.py @@ -4042,4 +4042,4 @@ class Guild(Hashable): """ data = await self._state.http.get_guild_onboarding(self.id) - return Onboarding(data=data, guild=self, state=self._state) \ No newline at end of file + return Onboarding(data=data, guild=self, state=self._state) From d051807865de3e00b9d0a292eb59c43b00b2a035 Mon Sep 17 00:00:00 2001 From: Josh <8677174+bijij@users.noreply.github.com> Date: Sat, 25 Feb 2023 21:13:59 +1000 Subject: [PATCH 03/20] Note that emoji can be Null --- discord/onboarding.py | 4 ++-- discord/types/emoji.py | 2 ++ discord/types/onboarding.py | 4 ++-- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/discord/onboarding.py b/discord/onboarding.py index c806541c6..fb28610f4 100644 --- a/discord/onboarding.py +++ b/discord/onboarding.py @@ -63,7 +63,7 @@ class OnboardingPromptOption: The title of this prompt option. description: Optional[:class:`str`] The description of this prompt option. - emoji: Union[:class:`Emoji`, :class:`PartialEmoji`, :class:`str`] + 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 set visible if this option is selected. @@ -90,7 +90,7 @@ class OnboardingPromptOption: self.id: int = int(data['id']) self.title: str = data['title'] self.description: Optional[str] = data['description'] - self.emoji: Union[PartialEmoji, Emoji, str] = self._state.get_reaction_emoji(data['emoji']) + self.emoji: Optional[Union[PartialEmoji, Emoji, str]] = self._state.get_reaction_emoji(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']} diff --git a/discord/types/emoji.py b/discord/types/emoji.py index d54690c14..85e709757 100644 --- a/discord/types/emoji.py +++ b/discord/types/emoji.py @@ -23,6 +23,7 @@ DEALINGS IN THE SOFTWARE. """ from typing import Optional, TypedDict +from typing_extensions import NotRequired from .snowflake import Snowflake, SnowflakeList from .user import User @@ -30,6 +31,7 @@ from .user import User class PartialEmoji(TypedDict): id: Optional[Snowflake] name: Optional[str] + animated: NotRequired[bool] class Emoji(PartialEmoji, total=False): diff --git a/discord/types/onboarding.py b/discord/types/onboarding.py index ab90f8533..11a85c8de 100644 --- a/discord/types/onboarding.py +++ b/discord/types/onboarding.py @@ -26,7 +26,7 @@ from __future__ import annotations from typing import Literal, Optional, TypedDict -from .emoji import Emoji +from .emoji import PartialEmoji from .snowflake import Snowflake @@ -37,7 +37,7 @@ class PromptOption(TypedDict): id: Snowflake channel_ids: list[Snowflake] role_ids: list[Snowflake] - emoji: Emoji + emoji: PartialEmoji title: str description: Optional[str] From 87c7a2abab09ae05daaaaa56f0eaa2b15f7ebc7f Mon Sep 17 00:00:00 2001 From: Josh Date: Mon, 27 Feb 2023 20:52:49 +1000 Subject: [PATCH 04/20] Update discord/onboarding.py Co-authored-by: numbermaniac <5206120+numbermaniac@users.noreply.github.com> --- discord/onboarding.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/discord/onboarding.py b/discord/onboarding.py index fb28610f4..ff0cfc699 100644 --- a/discord/onboarding.py +++ b/discord/onboarding.py @@ -66,9 +66,9 @@ class OnboardingPromptOption: 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 set visible if this option is selected. + 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 + The IDs of the roles given to the user if this option is selected. """ __slots__ = ( From 074b05f0b0ecc63fd002d998fe03888844d376ab Mon Sep 17 00:00:00 2001 From: Josh Date: Mon, 27 Feb 2023 22:25:42 +1000 Subject: [PATCH 05/20] Update discord/onboarding.py Co-authored-by: numbermaniac <5206120+numbermaniac@users.noreply.github.com> --- discord/onboarding.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/discord/onboarding.py b/discord/onboarding.py index ff0cfc699..96be087cd 100644 --- a/discord/onboarding.py +++ b/discord/onboarding.py @@ -99,7 +99,7 @@ class OnboardingPromptOption: @cached_slot_property('_cs_channels') def channels(self) -> List[Union[GuildChannel, Thread]]: - """List[Union[:class:`abc.GuildChannel`, :class:`Thread`]]: The list of channels which will be set visible if this option is selected.""" + """List[Union[:class:`abc.GuildChannel`, :class:`Thread`]]: The list of channels which will be made visible if this option is selected.""" it = filter(None, map(self.guild._resolve_channel, self.channel_ids)) return utils._unique(it) From d3fe6d633ceb953e8f7d9b1716a9816526aaba8f Mon Sep 17 00:00:00 2001 From: Josh <8677174+bijij@users.noreply.github.com> Date: Sat, 1 Apr 2023 22:09:58 +1000 Subject: [PATCH 06/20] Rename State.get_reaction_emoji --- discord/onboarding.py | 2 +- discord/reaction.py | 2 +- discord/state.py | 2 +- discord/webhook/async_.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/discord/onboarding.py b/discord/onboarding.py index 96be087cd..f4999b0f4 100644 --- a/discord/onboarding.py +++ b/discord/onboarding.py @@ -90,7 +90,7 @@ class OnboardingPromptOption: 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_reaction_emoji(data['emoji']) + 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']} diff --git a/discord/reaction.py b/discord/reaction.py index 5f50ec8f4..e1f88061f 100644 --- a/discord/reaction.py +++ b/discord/reaction.py @@ -85,7 +85,7 @@ class Reaction: def __init__(self, *, message: Message, data: ReactionPayload, emoji: Optional[Union[PartialEmoji, Emoji, str]] = None): self.message: Message = message - self.emoji: Union[PartialEmoji, Emoji, str] = emoji or message._state.get_reaction_emoji(data['emoji']) + self.emoji: Union[PartialEmoji, Emoji, str] = emoji or message._state.get_emoji_from_partial_payload(data['emoji']) self.count: int = data.get('count', 1) self.me: bool = data['me'] diff --git a/discord/state.py b/discord/state.py index 8b556f28c..68ca61f36 100644 --- a/discord/state.py +++ b/discord/state.py @@ -1572,7 +1572,7 @@ class ConnectionState(Generic[ClientT]): return channel.guild.get_member(user_id) return self.get_user(user_id) - def get_reaction_emoji(self, data: PartialEmojiPayload) -> Union[Emoji, PartialEmoji, str]: + def get_emoji_from_partial_payload(self, data: PartialEmojiPayload) -> Union[Emoji, PartialEmoji, str]: emoji_id = utils._get_as_snowflake(data, 'id') if not emoji_id: diff --git a/discord/webhook/async_.py b/discord/webhook/async_.py index f9b03193a..acf69c310 100644 --- a/discord/webhook/async_.py +++ b/discord/webhook/async_.py @@ -731,7 +731,7 @@ class _WebhookState: def get_reaction_emoji(self, data: PartialEmojiPayload) -> Union[PartialEmoji, Emoji, str]: if self._parent is not None: - return self._parent.get_reaction_emoji(data) + return self._parent.get_emoji_from_partial_payload(data) emoji_id = utils._get_as_snowflake(data, 'id') From f21c903ac783f7e600cd63168b1b104f0acce6e0 Mon Sep 17 00:00:00 2001 From: Josh <8677174+bijij@users.noreply.github.com> Date: Sat, 1 Apr 2023 22:16:52 +1000 Subject: [PATCH 07/20] Bump version --- discord/guild.py | 2 +- discord/onboarding.py | 6 +++--- docs/api.rst | 27 ++++++++++++++------------- 3 files changed, 18 insertions(+), 17 deletions(-) diff --git a/discord/guild.py b/discord/guild.py index 5593a88e5..8317b7a00 100644 --- a/discord/guild.py +++ b/discord/guild.py @@ -4227,7 +4227,7 @@ class Guild(Hashable): Fetches the onboarding configuration for this guild. - .. versionadded:: 2.2 + .. versionadded:: 2.3 Returns -------- diff --git a/discord/onboarding.py b/discord/onboarding.py index f4999b0f4..d06d778d7 100644 --- a/discord/onboarding.py +++ b/discord/onboarding.py @@ -51,7 +51,7 @@ if TYPE_CHECKING: class OnboardingPromptOption: """Represents an onboarding prompt option. - .. versionadded:: 2.2 + .. versionadded:: 2.3 Attributes ----------- @@ -113,7 +113,7 @@ class OnboardingPromptOption: class OnboardingPrompt: """Represents an onboarding prompt. - .. versionadded:: 2.2 + .. versionadded:: 2.3 Attributes ----------- @@ -167,7 +167,7 @@ class OnboardingPrompt: class Onboarding: """Represents a guild's onboarding configuration. - .. versionadded:: 2.2 + .. versionadded:: 2.3 Attributes ----------- diff --git a/docs/api.rst b/docs/api.rst index adc523680..992cf3941 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -3309,34 +3309,35 @@ of :class:`enum.Enum`. Displays posts as a collection of tiles. -.. class:: OnboardingPromptType - Represents the type of onboarding prompt. +.. class:: ForumOrderType - .. versionadded:: 2.2 + Represents how a forum's posts are sorted in the client. - .. attribute:: multiple_choice + .. versionadded:: 2.3 - Prompt options are multiple choice. + .. attribute:: latest_activity - .. attribute:: dropdown + Sort forum posts by activity. - Prompt options are displayed as a drop-down. + .. attribute:: creation_date + Sort forum posts by creation time (from most recent to oldest). -.. class:: ForumOrderType - Represents how a forum's posts are sorted in the client. +.. class:: OnboardingPromptType + + Represents the type of onboarding prompt. .. versionadded:: 2.3 - .. attribute:: latest_activity + .. attribute:: multiple_choice - Sort forum posts by activity. + Prompt options are multiple choice. - .. attribute:: creation_date + .. attribute:: dropdown - Sort forum posts by creation time (from most recent to oldest). + Prompt options are displayed as a drop-down. .. _discord-api-audit-logs: From e8462dffa06fe9cec4521dc4cd04a4fbbd26c278 Mon Sep 17 00:00:00 2001 From: Josh <8677174+bijij@users.noreply.github.com> Date: Tue, 18 Apr 2023 21:47:44 +1000 Subject: [PATCH 08/20] Address review comments --- discord/guild.py | 3 +-- discord/onboarding.py | 6 +++++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/discord/guild.py b/discord/guild.py index 8317b7a00..ea488c0ab 100644 --- a/discord/guild.py +++ b/discord/guild.py @@ -4222,7 +4222,7 @@ class Guild(Hashable): return AutoModRule(data=data, guild=self, state=self._state) - async def fetch_onboarding(self) -> Onboarding: + async def onboarding(self) -> Onboarding: """|coro| Fetches the onboarding configuration for this guild. @@ -4235,5 +4235,4 @@ class Guild(Hashable): The onboarding configuration that was fetched. """ data = await self._state.http.get_guild_onboarding(self.id) - return Onboarding(data=data, guild=self, state=self._state) diff --git a/discord/onboarding.py b/discord/onboarding.py index d06d778d7..0e47ddedf 100644 --- a/discord/onboarding.py +++ b/discord/onboarding.py @@ -30,7 +30,11 @@ from . import utils from .enums import OnboardingPromptType, try_enum from .utils import cached_slot_property -__all__ = ('Onboarding', 'OnboardingPrompt', 'OnboardingPromptOption') +__all__ = ( + 'Onboarding', + 'OnboardingPrompt', + 'OnboardingPromptOption', +) if TYPE_CHECKING: From e8019f9d4cbf16b1b52f39c2bd8251c600b8239d Mon Sep 17 00:00:00 2001 From: Josh <8677174+bijij@users.noreply.github.com> Date: Tue, 18 Apr 2023 22:11:15 +1000 Subject: [PATCH 09/20] Add Onboarding audit log data --- discord/audit_logs.py | 19 ++++++- discord/enums.py | 12 +++++ discord/types/audit_log.py | 29 +++++++++-- docs/api.rst | 102 +++++++++++++++++++++++++++++++++++-- 4 files changed, 155 insertions(+), 7 deletions(-) diff --git a/discord/audit_logs.py b/discord/audit_logs.py index 47f397a8a..fa5b2ff21 100644 --- a/discord/audit_logs.py +++ b/discord/audit_logs.py @@ -44,6 +44,7 @@ from .sticker import GuildSticker from .threads import Thread from .integrations import PartialIntegration from .channel import ForumChannel, StageChannel, ForumTag +from .onboarding import OnboardingPrompt, OnboardingPromptOption __all__ = ( 'AuditLogDiff', @@ -72,6 +73,7 @@ if TYPE_CHECKING: from .types.snowflake import Snowflake from .types.command import ApplicationCommandPermissions from .types.automod import AutoModerationTriggerMetadata, AutoModerationAction + from .types.onboarding import Prompt as PromptPayload, PromptOption as PromptOptionPayload from .user import User from .app_commands import AppCommand @@ -263,6 +265,16 @@ def _transform_automod_actions(entry: AuditLogEntry, data: List[AutoModerationAc return [AutoModRuleAction.from_data(action) for action in data] +def _transform_onboarding_prompts(entry: AuditLogEntry, data: List[PromptPayload]) -> List[OnboardingPrompt]: + return [OnboardingPrompt(data=prompt, state=entry._state, guild=entry.guild) for prompt in data] + + +def _transform_onboarding_prompt_options( + entry: AuditLogEntry, data: List[PromptOptionPayload] +) -> List[OnboardingPromptOption]: + return [OnboardingPromptOption(data=option, state=entry._state, guild=entry.guild) for option in data] + + E = TypeVar('E', bound=enums.Enum) @@ -285,13 +297,15 @@ def _flag_transformer(cls: Type[F]) -> Callable[[AuditLogEntry, Union[int, str]] def _transform_type( entry: AuditLogEntry, data: Union[int, str] -) -> Union[enums.ChannelType, enums.StickerType, enums.WebhookType, str]: +) -> Union[enums.ChannelType, enums.StickerType, enums.WebhookType, str, enums.OnboardingPromptType]: if entry.action.name.startswith('sticker_'): return enums.try_enum(enums.StickerType, data) elif entry.action.name.startswith('integration_'): return data # type: ignore # integration type is str elif entry.action.name.startswith('webhook_'): return enums.try_enum(enums.WebhookType, data) + elif entry.action.name.startswith('onboarding_question_'): + return enums.try_enum(enums.OnboardingPromptType, data) else: return enums.try_enum(enums.ChannelType, data) @@ -370,6 +384,9 @@ class AuditLogChanges: 'available_tags': (None, _transform_forum_tags), 'flags': (None, _transform_overloaded_flags), 'default_reaction_emoji': (None, _transform_default_reaction), + 'options': (None, _transform_onboarding_prompt_options), + 'prompts': (None, _transform_onboarding_prompts), + 'default_channel_ids': ('default_channels', _transform_channels_or_threads), } # fmt: on diff --git a/discord/enums.py b/discord/enums.py index e0db03669..b8e0bc36d 100644 --- a/discord/enums.py +++ b/discord/enums.py @@ -368,6 +368,11 @@ class AuditLogAction(Enum): automod_block_message = 143 automod_flag_message = 144 automod_timeout_member = 145 + onboarding_question_create = 163 + onboarding_question_update = 164 + onboarding_update = 167 + server_guide_create = 190 + server_guide_update = 191 # fmt: on @property @@ -428,6 +433,9 @@ class AuditLogAction(Enum): AuditLogAction.automod_block_message: None, AuditLogAction.automod_flag_message: None, AuditLogAction.automod_timeout_member: None, + AuditLogAction.onboarding_question_create: AuditLogActionCategory.create, + AuditLogAction.onboarding_question_update: AuditLogActionCategory.update, + AuditLogAction.onboarding_update: AuditLogActionCategory.update, } # fmt: on return lookup[self] @@ -471,6 +479,10 @@ class AuditLogAction(Enum): return 'auto_moderation' elif v < 146: return 'user' + elif v < 165: + return 'onboarding_question' + elif v < 168: + return 'onboarding' class UserFlags(Enum): diff --git a/discord/types/audit_log.py b/discord/types/audit_log.py index 4401bc784..b46a37274 100644 --- a/discord/types/audit_log.py +++ b/discord/types/audit_log.py @@ -37,6 +37,7 @@ from .role import Role from .channel import ChannelType, DefaultReaction, PrivacyLevel, VideoQualityMode, PermissionOverwrite, ForumTag from .threads import Thread from .command import ApplicationCommand, ApplicationCommandPermissions +from .onboarding import PromptOption, Prompt AuditLogEvent = Literal[ 1, @@ -93,6 +94,9 @@ AuditLogEvent = Literal[ 143, 144, 145, + 163, + 164, + 167, ] @@ -109,6 +113,7 @@ class _AuditLogChange_Str(TypedDict): 'permissions', 'tags', 'unicode_emoji', + 'title', ] new_value: str old_value: str @@ -154,6 +159,10 @@ class _AuditLogChange_Bool(TypedDict): 'available', 'archived', 'locked', + 'enabled', + 'single_select', + 'required', + 'in_onboarding', ] new_value: bool old_value: bool @@ -258,8 +267,8 @@ class _AuditLogChange_AppCommandPermissions(TypedDict): old_value: ApplicationCommandPermissions -class _AuditLogChange_AppliedTags(TypedDict): - key: Literal['applied_tags'] +class _AuditLogChange_SnowflakeList(TypedDict): + key: Literal['applied_tags', 'default_channel_ids'] new_value: List[Snowflake] old_value: List[Snowflake] @@ -276,6 +285,18 @@ class _AuditLogChange_DefaultReactionEmoji(TypedDict): old_value: Optional[DefaultReaction] +class _AuditLogChange_Prompts(TypedDict): + key: Literal['prompts'] + new_value: List[Prompt] + old_value: List[Prompt] + + +class _AuditLogChange_Options(TypedDict): + key: Literal['options'] + new_value: List[PromptOption] + old_value: List[PromptOption] + + AuditLogChange = Union[ _AuditLogChange_Str, _AuditLogChange_AssetHash, @@ -295,9 +316,11 @@ AuditLogChange = Union[ _AuditLogChange_Status, _AuditLogChange_EntityType, _AuditLogChange_AppCommandPermissions, - _AuditLogChange_AppliedTags, + _AuditLogChange_SnowflakeList, _AuditLogChange_AvailableTags, _AuditLogChange_DefaultReactionEmoji, + _AuditLogChange_Prompts, + _AuditLogChange_Options, ] diff --git a/docs/api.rst b/docs/api.rst index 992cf3941..f63eb38a8 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -2798,6 +2798,46 @@ of :class:`enum.Enum`. .. versionadded:: 2.1 + .. attribute:: onboarding_question_create + + A guild onboarding prompt was created. + + Possible attributes for :class:`AuditLogDiff`: + + - :attr:`~AuditLogDiff.type` + - :attr:`~AuditLogDiff.title` + - :attr:`~AuditLogDiff.options` + - :attr:`~AuditLogDiff.single_select` + - :attr:`~AuditLogDiff.required` + - :attr:`~AuditLogDiff.in_onboarding` + + .. versionadded:: 2.3 + + .. attribute:: onboarding_question_update + + A guild onboarding prompt was updated. + + Possible attributes for :class:`AuditLogDiff`: + + - :attr:`~AuditLogDiff.type` + - :attr:`~AuditLogDiff.title` + - :attr:`~AuditLogDiff.options` + - :attr:`~AuditLogDiff.single_select` + - :attr:`~AuditLogDiff.required` + - :attr:`~AuditLogDiff.in_onboarding` + + .. versionadded:: 2.3 + + .. attribute:: onboarding_update + + The guild's onboarding configuration was updated. + + Possible attributes for :class:`AuditLogDiff`: + + - :attr:`~AuditLogDiff.enabled` + - :attr:`~AuditLogDiff.default_channels` + - :attr:`~AuditLogDiff.prompts` + .. class:: AuditLogActionCategory Represents the category that the :class:`AuditLogAction` belongs to. @@ -3580,9 +3620,9 @@ AuditLogDiff .. attribute:: type - The type of channel, sticker, webhook or integration. + The type of channel, sticker, webhook, integration or onboarding prompt. - :type: Union[:class:`ChannelType`, :class:`StickerType`, :class:`WebhookType`, :class:`str`] + :type: Union[:class:`ChannelType`, :class:`StickerType`, :class:`WebhookType`, :class:`str`, :class:`OnboardingPromptType`] .. attribute:: topic @@ -3935,7 +3975,7 @@ AuditLogDiff .. attribute:: enabled - Whether the automod rule is active or not. + Whether guild onboarding or the automod rule is active or not. :type: :class:`bool` @@ -4011,6 +4051,62 @@ AuditLogDiff :type: :class:`ChannelFlags` + .. attribute:: options + + The onboarding prompt options associated with this onboarding prompt. + + See also :attr:`OnboardingPrompt.options` + + :type: List[:class:`OnboardingPromptOption`] + + .. attribute:: default_channels + + The default channels associated with the onboarding in this guild. + + See also :attr:`Onboarding.default_channels` + + :type: List[:class:`abc.GuildChannel`, :class:`Object`] + + .. attribute:: prompts + + The onboarding prompts associated with the onboarding in this guild. + + See also :attr:`Onboarding.prompts` + + :type: List[:class:`OnboardingPrompt`] + + .. attribute:: title + + The title of the onboarding prompt. + + See also :attr:`OnboardingPrompt.title` + + :type: :class:`str` + + .. attribute:: single_select + + Whether only one prompt option can be selected. + + See also :attr:`OnboardingPrompt.single_select` + + :type: :class:`bool` + + .. attribute:: required + + Whether the onboarding prompt is required to complete the onboarding. + + See also :attr:`OnboardingPrompt.required` + + :type: :class:`bool` + + .. attribute:: in_onboarding + + Whether this prompt is currently part of the onboarding flow. + + See also :attr:`OnboardingUser.in_onboarding` + + :type: :class:`bool` + .. this is currently missing the following keys: reason and application_id I'm not sure how to port these From 689cc855331717708eaaa5fc56becab5c0123176 Mon Sep 17 00:00:00 2001 From: Josh <8677174+bijij@users.noreply.github.com> Date: Sun, 2 Jul 2023 01:33:00 +1000 Subject: [PATCH 10/20] Update versions, fix docs --- discord/guild.py | 2 +- discord/onboarding.py | 6 +++--- docs/api.rst | 10 ++++++---- 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/discord/guild.py b/discord/guild.py index 9ced6114b..87c97b488 100644 --- a/discord/guild.py +++ b/discord/guild.py @@ -4284,7 +4284,7 @@ class Guild(Hashable): Fetches the onboarding configuration for this guild. - .. versionadded:: 2.3 + .. versionadded:: 2.4 Returns -------- diff --git a/discord/onboarding.py b/discord/onboarding.py index 0e47ddedf..4a96444ec 100644 --- a/discord/onboarding.py +++ b/discord/onboarding.py @@ -55,7 +55,7 @@ if TYPE_CHECKING: class OnboardingPromptOption: """Represents an onboarding prompt option. - .. versionadded:: 2.3 + .. versionadded:: 2.4 Attributes ----------- @@ -117,7 +117,7 @@ class OnboardingPromptOption: class OnboardingPrompt: """Represents an onboarding prompt. - .. versionadded:: 2.3 + .. versionadded:: 2.4 Attributes ----------- @@ -171,7 +171,7 @@ class OnboardingPrompt: class Onboarding: """Represents a guild's onboarding configuration. - .. versionadded:: 2.3 + .. versionadded:: 2.4 Attributes ----------- diff --git a/docs/api.rst b/docs/api.rst index 06c81120a..a5ec43a57 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -2812,7 +2812,7 @@ of :class:`enum.Enum`. - :attr:`~AuditLogDiff.required` - :attr:`~AuditLogDiff.in_onboarding` - .. versionadded:: 2.3 + .. versionadded:: 2.4 .. attribute:: onboarding_question_update @@ -2827,7 +2827,7 @@ of :class:`enum.Enum`. - :attr:`~AuditLogDiff.required` - :attr:`~AuditLogDiff.in_onboarding` - .. versionadded:: 2.3 + .. versionadded:: 2.4 .. attribute:: onboarding_update @@ -2839,6 +2839,8 @@ of :class:`enum.Enum`. - :attr:`~AuditLogDiff.default_channels` - :attr:`~AuditLogDiff.prompts` + .. versionadded:: 2.4 + .. class:: AuditLogActionCategory Represents the category that the :class:`AuditLogAction` belongs to. @@ -3376,7 +3378,7 @@ of :class:`enum.Enum`. Represents the type of onboarding prompt. - .. versionadded:: 2.3 + .. versionadded:: 2.4 .. attribute:: multiple_choice @@ -4148,7 +4150,7 @@ AuditLogDiff Whether this prompt is currently part of the onboarding flow. - See also :attr:`OnboardingUser.in_onboarding` + See also :attr:`OnboardingPrompt.in_onboarding` :type: :class:`bool` From a9ed9efedb9cc40fce65f43b5221c72966384744 Mon Sep 17 00:00:00 2001 From: Josh <8677174+bijij@users.noreply.github.com> Date: Sun, 2 Jul 2023 01:39:09 +1000 Subject: [PATCH 11/20] Fix PromptOption roles property --- discord/onboarding.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/discord/onboarding.py b/discord/onboarding.py index 4a96444ec..e400cd6c6 100644 --- a/discord/onboarding.py +++ b/discord/onboarding.py @@ -108,8 +108,8 @@ class OnboardingPromptOption: return utils._unique(it) @cached_slot_property('_cs_roles') - def default_channels(self) -> List[Role]: - """List[Union[:class:`abc.GuildChannel`, :class:`Thread`]]: The list of roles given to the user if this option is selected.""" + def roles(self) -> List[Role]: + """List[:class:`Role`]: The list of roles given to the user if this option is selected.""" it = filter(None, map(self.guild.get_role, self.role_ids)) return utils._unique(it) From 1b29dd36cbd440f0ffe48d2d128ea53eb2259f97 Mon Sep 17 00:00:00 2001 From: Josh <8677174+bijij@users.noreply.github.com> Date: Sun, 2 Jul 2023 03:05:16 +1000 Subject: [PATCH 12/20] Add support for modifying onboarding, mode --- discord/enums.py | 6 ++ discord/guild.py | 53 ++++++++++- discord/http.py | 31 +++++++ discord/onboarding.py | 177 +++++++++++++++++++++++++++++++----- discord/state.py | 8 ++ discord/types/onboarding.py | 2 + docs/api.rst | 30 ++++++ 7 files changed, 282 insertions(+), 25 deletions(-) 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 ~~~~~~~~~~~~~~~~~~~~~~~ From cf27b27c0b2dbba2b33f064ab5cbe04e8ab74e78 Mon Sep 17 00:00:00 2001 From: Josh <8677174+bijij@users.noreply.github.com> Date: Sun, 2 Jul 2023 03:07:50 +1000 Subject: [PATCH 13/20] Remove types import --- discord/onboarding.py | 1 - 1 file changed, 1 deletion(-) diff --git a/discord/onboarding.py b/discord/onboarding.py index b88b21d9b..3b99cd90d 100644 --- a/discord/onboarding.py +++ b/discord/onboarding.py @@ -27,7 +27,6 @@ from __future__ import annotations 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 786b153242c7e5d70862d9a3ef775be0025599fc Mon Sep 17 00:00:00 2001 From: Josh <8677174+bijij@users.noreply.github.com> Date: Sun, 2 Jul 2023 03:10:11 +1000 Subject: [PATCH 14/20] Resolve circular import --- discord/onboarding.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/discord/onboarding.py b/discord/onboarding.py index 3b99cd90d..5fcf234b2 100644 --- a/discord/onboarding.py +++ b/discord/onboarding.py @@ -32,7 +32,6 @@ from discord.utils import MISSING from . import utils from .mixins import Hashable from .enums import OnboardingMode, OnboardingPromptType, try_enum -from .state import ConnectionState from .utils import cached_slot_property, MISSING __all__ = ( @@ -56,6 +55,7 @@ if TYPE_CHECKING: PromptOption as PromptOptionPayload, Onboarding as OnboardingPayload, ) + from .state import ConnectionState class PartialOnboardingPromptOption: @@ -101,6 +101,7 @@ class PartialOnboardingPromptOption: self.role_ids: Set[int] = set(role_ids or []) def to_dict(self, *, id: int = MISSING) -> PromptOptionPayload: + from .state import ConnectionState # circular import return { 'id': id or os.urandom(16).hex(), 'title': self.title, From 96778d320cae6de25e6f5634d993884140dd2d99 Mon Sep 17 00:00:00 2001 From: Josh <8677174+bijij@users.noreply.github.com> Date: Sun, 2 Jul 2023 03:10:27 +1000 Subject: [PATCH 15/20] Fix spacing --- discord/onboarding.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/discord/onboarding.py b/discord/onboarding.py index 5fcf234b2..1832984a2 100644 --- a/discord/onboarding.py +++ b/discord/onboarding.py @@ -101,7 +101,7 @@ class PartialOnboardingPromptOption: self.role_ids: Set[int] = set(role_ids or []) def to_dict(self, *, id: int = MISSING) -> PromptOptionPayload: - from .state import ConnectionState # circular import + from .state import ConnectionState # circular import return { 'id': id or os.urandom(16).hex(), 'title': self.title, From b0a281aa5ccce35ed79b18f2952d8fcf49b38d02 Mon Sep 17 00:00:00 2001 From: Josh <8677174+bijij@users.noreply.github.com> Date: Sun, 2 Jul 2023 03:12:06 +1000 Subject: [PATCH 16/20] run black --- discord/onboarding.py | 1 + 1 file changed, 1 insertion(+) diff --git a/discord/onboarding.py b/discord/onboarding.py index 1832984a2..50bfeddeb 100644 --- a/discord/onboarding.py +++ b/discord/onboarding.py @@ -102,6 +102,7 @@ class PartialOnboardingPromptOption: def to_dict(self, *, id: int = MISSING) -> PromptOptionPayload: from .state import ConnectionState # circular import + return { 'id': id or os.urandom(16).hex(), 'title': self.title, From 28fc9f0607e4f22eca866b98971fe9216e5b873e Mon Sep 17 00:00:00 2001 From: Josh <8677174+bijij@users.noreply.github.com> Date: Sun, 2 Jul 2023 03:13:02 +1000 Subject: [PATCH 17/20] Fix underline on prompt option --- docs/api.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api.rst b/docs/api.rst index 3f0e00a93..327edd878 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -4761,7 +4761,7 @@ OnboardingPrompt :members: PartialOnboardingPromptOption -~~~~~~~~~~~~~~~~~~~~~~~~ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. attributetable:: PartialOnboardingPromptOption From 025d09937916e55acc41b3e448e4edaf7115f0a2 Mon Sep 17 00:00:00 2001 From: Josh <8677174+bijij@users.noreply.github.com> Date: Wed, 26 Jul 2023 23:50:14 +1000 Subject: [PATCH 18/20] Fix prompt ID code --- discord/guild.py | 2 +- discord/onboarding.py | 7 ++----- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/discord/guild.py b/discord/guild.py index 60e8c9cd0..d25af2f09 100644 --- a/discord/guild.py +++ b/discord/guild.py @@ -4337,7 +4337,7 @@ class Guild(Hashable): """ 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, + prompts=[p.to_dict(id=i) for i, p in enumerate(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, diff --git a/discord/onboarding.py b/discord/onboarding.py index 50bfeddeb..4ca71b724 100644 --- a/discord/onboarding.py +++ b/discord/onboarding.py @@ -223,9 +223,9 @@ class PartialOnboardingPrompt: self.required: bool = required self.in_onboarding: bool = in_onboarding - def to_dict(self, *, id: int = MISSING) -> PromptPayload: + def to_dict(self, *, id: int) -> PromptPayload: return { - 'id': id or os.urandom(16).hex(), + 'id': id, 'type': self.type.value, 'title': self.title, 'options': [option.to_dict() for option in self.options], @@ -290,9 +290,6 @@ class OnboardingPrompt(PartialOnboardingPrompt, Hashable): 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. From b805441fc7b00117d73bc410e0caf04268cf60ae Mon Sep 17 00:00:00 2001 From: Josh <8677174+bijij@users.noreply.github.com> Date: Fri, 10 Nov 2023 00:08:13 +1000 Subject: [PATCH 19/20] Update guild.py Co-Authored-By: Andrin <65789180+Puncher1@users.noreply.github.com> --- discord/guild.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/discord/guild.py b/discord/guild.py index 395fa59fa..ae34cf9a2 100644 --- a/discord/guild.py +++ b/discord/guild.py @@ -4350,7 +4350,7 @@ class Guild(Hashable): :class:`Onboarding` The new onboarding configuration. """ - data = await self._state.http.modify_guild_onboarding( + data = await self._state.http.edit_guild_onboarding( self.id, prompts=[p.to_dict(id=i) for i, p in enumerate(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, From 001dc937017af60f0c1769186307bd87d4386f4a Mon Sep 17 00:00:00 2001 From: Josh <8677174+bijij@users.noreply.github.com> Date: Fri, 10 Nov 2023 00:19:16 +1000 Subject: [PATCH 20/20] Move to the dumbest emoji payload I've ever seen --- discord/onboarding.py | 13 ++++++++++--- discord/state.py | 8 -------- 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/discord/onboarding.py b/discord/onboarding.py index 4ca71b724..5b5c59e0b 100644 --- a/discord/onboarding.py +++ b/discord/onboarding.py @@ -101,16 +101,23 @@ class PartialOnboardingPromptOption: self.role_ids: Set[int] = set(role_ids or []) def to_dict(self, *, id: int = MISSING) -> PromptOptionPayload: - from .state import ConnectionState # circular import + if isinstance(self.emoji, str): + emoji_payload = {"emoji_name": self.emoji} + else: + emoji_payload = { + "emoji_id": self.emoji.id, + "emoji_name": self.emoji.name, + "emoji_animated": self.emoji.animated, + } 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), - } + **emoji_payload, + } # type: ignore class OnboardingPromptOption(PartialOnboardingPromptOption, Hashable): diff --git a/discord/state.py b/discord/state.py index aaa591ee7..ed956eb50 100644 --- a/discord/state.py +++ b/discord/state.py @@ -1616,14 +1616,6 @@ 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: