From f92a63ad16ba14a7212ba9159bbcfeebbdb94842 Mon Sep 17 00:00:00 2001 From: Soheab_ <33902984+Soheab@users.noreply.github.com> Date: Thu, 10 Jul 2025 00:37:19 +0200 Subject: [PATCH] Final changes - Remove partials - Fix AuditLogAction docs - Add AuditLogDiff.mode - Add Onboarding.get_prompt - Add OnboardingPrompt.get_option - Add PartialEmoji._to_onboarding_prompt_option_payload --- discord/audit_logs.py | 5 +- discord/guild.py | 9 +- discord/onboarding.py | 275 +++++++++++++++++------------------- discord/partial_emoji.py | 6 + discord/types/onboarding.py | 23 ++- docs/api.rst | 61 +------- 6 files changed, 166 insertions(+), 213 deletions(-) diff --git a/discord/audit_logs.py b/discord/audit_logs.py index a5fbda7c9..aceaf367d 100644 --- a/discord/audit_logs.py +++ b/discord/audit_logs.py @@ -249,13 +249,13 @@ def _transform_default_emoji(entry: AuditLogEntry, data: str) -> PartialEmoji: 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] + return [OnboardingPrompt.from_dict(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] + return [OnboardingPromptOption.from_dict(data=option, state=entry._state, guild=entry.guild) for option in data] E = TypeVar('E', bound=enums.Enum) @@ -371,6 +371,7 @@ class AuditLogChanges: 'options': (None, _transform_onboarding_prompt_options), 'prompts': (None, _transform_onboarding_prompts), 'default_channel_ids': ('default_channels', _transform_channels_or_threads), + 'mode': (None, _enum_transformer(enums.OnboardingMode)), } # fmt: on diff --git a/discord/guild.py b/discord/guild.py index 183f28c8e..cbf28ffc3 100644 --- a/discord/guild.py +++ b/discord/guild.py @@ -92,7 +92,7 @@ from .sticker import GuildSticker from .file import File from .audit_logs import AuditLogEntry from .object import OLDEST_OBJECT, Object -from .onboarding import Onboarding, PartialOnboardingPrompt +from .onboarding import Onboarding from .welcome_screen import WelcomeScreen, WelcomeChannel from .automod import AutoModRule, AutoModTrigger, AutoModRuleAction from .partial_emoji import _EmojiTag, PartialEmoji @@ -141,6 +141,7 @@ if TYPE_CHECKING: from .types.widget import EditWidgetSettings from .types.audit_log import AuditLogEvent from .message import EmojiInputType + from .onboarding import OnboardingPrompt VocalGuildChannel = Union[VoiceChannel, StageChannel] GuildChannel = Union[VocalGuildChannel, ForumChannel, TextChannel, CategoryChannel] @@ -4875,7 +4876,7 @@ class Guild(Hashable): Fetches the onboarding configuration for this guild. - You must have :attr:`Permissions.manage_guild` and + You must have :attr:`Permissions.manage_guild` and :attr:`Permissions.manage_roles` to do this. .. versionadded:: 2.6 @@ -4891,7 +4892,7 @@ class Guild(Hashable): async def edit_onboarding( self, *, - prompts: List[PartialOnboardingPrompt] = MISSING, + prompts: List[OnboardingPrompt] = MISSING, default_channels: List[Snowflake] = MISSING, enabled: bool = MISSING, mode: OnboardingMode = MISSING, @@ -4905,7 +4906,7 @@ class Guild(Hashable): Parameters ----------- - prompts: List[:class:`PartialOnboardingPrompt`] + prompts: List[:class:`OnboardingPrompt`] 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. diff --git a/discord/onboarding.py b/discord/onboarding.py index bc3a72c7b..6beec0c75 100644 --- a/discord/onboarding.py +++ b/discord/onboarding.py @@ -23,28 +23,25 @@ DEALINGS IN THE SOFTWARE. """ from __future__ import annotations - -import os from typing import TYPE_CHECKING, Iterable, Optional, Set, List, Union -from discord.utils import MISSING - -from . import utils from .mixins import Hashable from .enums import OnboardingMode, OnboardingPromptType, try_enum +from .partial_emoji import PartialEmoji from .utils import cached_slot_property, MISSING +from . import utils __all__ = ( 'Onboarding', - 'PartialOnboardingPrompt', 'OnboardingPrompt', 'OnboardingPromptOption', - 'PartialOnboardingPromptOption', ) if TYPE_CHECKING: - from .abc import GuildChannel + from typing_extensions import Self + + from .abc import GuildChannel, Snowflake from .emoji import Emoji from .guild import Guild from .partial_emoji import PartialEmoji @@ -53,19 +50,23 @@ if TYPE_CHECKING: from .types.onboarding import ( Prompt as PromptPayload, PromptOption as PromptOptionPayload, + CreatePromptOption as CreatePromptOptionPayload, Onboarding as OnboardingPayload, ) from .state import ConnectionState -class PartialOnboardingPromptOption: - """Represents a partial onboarding prompt option, these are used in the creation - of an :class:`OnboardingPrompt` via :meth:`Guild.edit_onboarding`. +class OnboardingPromptOption(Hashable): + """Represents a onboarding prompt option. + + This can be manually created for :meth:`Guild.edit_onboarding`. .. versionadded:: 2.6 Attributes ----------- + id: :class:`int` + The ID of this prompt option. If this was manually created then the ID will be ``0``. title: :class:`str` The title of this prompt option. description: Optional[:class:`str`] @@ -73,126 +74,123 @@ class PartialOnboardingPromptOption: 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. + The IDs of the channels the user will be added to 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 the user will be given if this option is selected. """ __slots__ = ( 'title', 'emoji', 'description', + 'id', 'channel_ids', 'role_ids', + '_guild', + '_cs_channels', + '_cs_roles', ) def __init__( self, + *, title: str, emoji: Union[Emoji, PartialEmoji, str] = MISSING, description: Optional[str] = None, - channel_ids: Iterable[int] = MISSING, - role_ids: Iterable[int] = MISSING, + channels: Iterable[Union[Snowflake, int]] = MISSING, + roles: Iterable[Union[Snowflake, int]] = MISSING, ) -> None: + self.id: int = 0 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: - if self.emoji is not MISSING: - 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, - } - else: - emoji_payload = {} - - return { - 'id': id or os.urandom(16).hex(), - 'title': self.title, - 'description': self.description, - 'channel_ids': list(self.channel_ids), - 'role_ids': list(self.role_ids), - **emoji_payload, - } # type: ignore - - -class OnboardingPromptOption(PartialOnboardingPromptOption, Hashable): - """Represents an onboarding prompt option. - - .. versionadded:: 2.6 + self.emoji: Optional[Union[Emoji, PartialEmoji]] = ( + PartialEmoji.from_str(emoji) if isinstance(emoji, str) else emoji if emoji is not MISSING else None + ) - 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: 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. - """ + self.channel_ids: Set[int] = ( + {c.id if not isinstance(c, int) else c for c in channels} if channels is not MISSING else set() + ) + self.role_ids: Set[int] = {c.id if not isinstance(c, int) else c for c in roles} if roles is not MISSING else set() + self._guild: Optional[Guild] = None - __slots__ = ( - '_state', - '_cs_channels', - '_cs_roles', - 'guild', - 'id', - ) + def __repr__(self) -> str: + return f'' - def __init__(self, *, data: PromptOptionPayload, state: ConnectionState, guild: Guild) -> None: - self._state: ConnectionState = state - self.guild: Guild = guild - self.id: int = int(data['id']) - super().__init__( + @classmethod + def from_dict(cls, *, data: PromptOptionPayload, state: ConnectionState, guild: Guild) -> Self: + instance = cls( 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']], + emoji=state.get_emoji_from_partial_payload(data['emoji']) if 'emoji' in data else MISSING, + channels=[int(id) for id in data['channel_ids']], + roles=[int(id) for id in data['role_ids']], ) + instance._guild = guild + instance.id = int(data['id']) + return instance - def __repr__(self) -> str: - return f'' + def to_dict( + self, + ) -> CreatePromptOptionPayload: + res: CreatePromptOptionPayload = { + 'title': self.title, + 'description': self.description, + 'channel_ids': list(self.channel_ids), + 'role_ids': list(self.role_ids), + } + if self.emoji: + res.update((self.emoji._to_partial())._to_onboarding_prompt_option_payload()) # type: ignore + return res + + @property + def guild(self) -> Guild: + """:class:`Guild`: The guild this prompt option is related to. + + Raises + ------- + ValueError + If this prompt option is manually created. + """ + if self._guild is None: + raise ValueError('This prompt option is manually created therefore has no guild.') + return self._guild @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 made 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. + + Raises + ------- + ValueError + IF the prompt option is manually created, therefore has no guild. + """ it = filter(None, map(self.guild._resolve_channel, self.channel_ids)) return utils._unique(it) @cached_slot_property('_cs_roles') def roles(self) -> List[Role]: - """List[:class:`Role`]: The list of roles given to the user if this option is selected.""" + """List[:class:`Role`]: The list of roles given to the user if this option is selected. + + Raises + ------- + ValueError + If the prompt option is manually created, therefore has no guild. + """ 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 OnboardingPrompt: + """Represents a onboarding prompt. -class PartialOnboardingPrompt: - """Represents a partial onboarding prompt, these are used in the creation - of an :class:`Onboarding` via :meth:`Guild.edit_onboarding`. + This can be manually created for :meth:`Guild.edit_onboarding`. .. versionadded:: 2.6 Attributes ----------- + id: :class:`int` + The ID of this prompt. If this was manually created then the ID will be ``0 type: :class:`OnboardingPromptType` The type of this prompt. title: :class:`str` @@ -208,12 +206,14 @@ class PartialOnboardingPrompt: """ __slots__ = ( + 'id', 'type', 'title', 'options', 'single_select', 'required', 'in_onboarding', + '_guild', ) def __init__( @@ -221,18 +221,40 @@ class PartialOnboardingPrompt: *, type: OnboardingPromptType, title: str, - options: List[PartialOnboardingPromptOption], + options: List[OnboardingPromptOption], single_select: bool = True, required: bool = True, in_onboarding: bool = True, ) -> None: - self.type: OnboardingPromptType = try_enum(OnboardingPromptType, type) + self.id: int = 0 + self.type: OnboardingPromptType = type self.title: str = title - self.options: List[PartialOnboardingPromptOption] = options + self.options: List[OnboardingPromptOption] = options self.single_select: bool = single_select self.required: bool = required self.in_onboarding: bool = in_onboarding + self._guild: Optional[Guild] = None + + def __repr__(self) -> str: + return f'' + + @classmethod + def from_dict(cls, *, data: PromptPayload, state: ConnectionState, guild: Guild) -> Self: + instance = cls( + type=try_enum(OnboardingPromptType, data['type']), + title=data['title'], + options=[ + OnboardingPromptOption.from_dict(data=option_data, state=state, guild=guild) # type: ignore + for option_data in data['options'] + ], + single_select=data['single_select'], + required=data['required'], + in_onboarding=data['in_onboarding'], + ) + instance.id = int(data['id']) + return instance + def to_dict(self, *, id: int) -> PromptPayload: return { 'id': id, @@ -244,61 +266,22 @@ class PartialOnboardingPrompt: 'in_onboarding': self.in_onboarding, } + @property + def guild(self) -> Guild: + """:class:`Guild`: The guild this prompt is related to. -class OnboardingPrompt(PartialOnboardingPrompt, Hashable): - """Represents an onboarding prompt. - - .. versionadded:: 2.6 - - 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. - """ - - options: List[OnboardingPromptOption] - - __slots__ = ( - '_state', - 'guild', - 'id', - 'title', - 'options', - 'single_select', - 'required', - 'in_onboarding', - 'type', - ) + Raises + ------ + ValueError + If this prompt is manually created, therefore has no guild. + """ + if self._guild is None: + raise ValueError('This prompt is manually created therefore has no guild.') + return self._guild - def __init__(self, *, data: PromptPayload, state: ConnectionState, guild: Guild): - self._state: ConnectionState = state - self.guild: Guild = guild - self.id: int = int(data['id']) - 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 get_option(self, option_id: int, /) -> Optional[OnboardingPromptOption]: + """Optional[:class:`OnboardingPromptOption`]: The option with the given ID, if found.""" + return next((option for option in self.options if option.id == option_id), None) class Onboarding: @@ -335,7 +318,7 @@ class Onboarding: 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'] + OnboardingPrompt.from_dict(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)) @@ -348,3 +331,7 @@ class Onboarding: """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) + + def get_prompt(self, prompt_id: int, /) -> Optional[OnboardingPrompt]: + """Optional[:class:`OnboardingPrompt`]: The prompt with the given ID, if found.""" + return next((prompt for prompt in self.prompts if prompt.id == prompt_id), None) diff --git a/discord/partial_emoji.py b/discord/partial_emoji.py index 7d366949c..502202330 100644 --- a/discord/partial_emoji.py +++ b/discord/partial_emoji.py @@ -167,6 +167,12 @@ class PartialEmoji(_EmojiTag, AssetMixin): return {'emoji_id': self.id, 'emoji_name': None} return {'emoji_id': None, 'emoji_name': self.name} + def _to_onboarding_prompt_option_payload(self) -> Dict[str, Any]: + if self.id is not None: + return {'emoji_id': self.id, 'emoji_name': self.name, 'emoji_animated': self.animated} + + return {'emoji_name': self.name} + @classmethod def with_state( cls, diff --git a/discord/types/onboarding.py b/discord/types/onboarding.py index 4945fb18e..64f9c45c8 100644 --- a/discord/types/onboarding.py +++ b/discord/types/onboarding.py @@ -23,29 +23,40 @@ DEALINGS IN THE SOFTWARE. """ from __future__ import annotations - -from typing import Literal, Optional, TypedDict, List +from typing import TYPE_CHECKING, Literal, Optional, TypedDict, List, Union from .emoji import PartialEmoji from .snowflake import Snowflake +if TYPE_CHECKING: + from typing_extensions import NotRequired + PromptType = Literal[0, 1] OnboardingMode = Literal[0, 1] -class PromptOption(TypedDict): - id: Snowflake +class _PromptOption(TypedDict): channel_ids: List[Snowflake] role_ids: List[Snowflake] - emoji: PartialEmoji title: str description: Optional[str] +class CreatePromptOption(_PromptOption): + emoji_id: NotRequired[Snowflake] + emoji_name: NotRequired[str] + emoji_animated: NotRequired[bool] + + +class PromptOption(_PromptOption): + id: Snowflake + emoji: NotRequired[PartialEmoji] + + class Prompt(TypedDict): id: Snowflake - options: List[PromptOption] + options: List[Union[PromptOption, CreatePromptOption]] title: str single_select: bool required: bool diff --git a/docs/api.rst b/docs/api.rst index 54ae9c65e..f5510dc18 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -3157,6 +3157,7 @@ of :class:`enum.Enum`. - :attr:`~AuditLogDiff.enabled` - :attr:`~AuditLogDiff.default_channels` - :attr:`~AuditLogDiff.prompts` + - :attr:`~AuditLogDiff.mode` .. versionadded:: 2.6 @@ -3169,6 +3170,7 @@ of :class:`enum.Enum`. - :attr:`~AuditLogDiff.enabled` - :attr:`~AuditLogDiff.default_channels` - :attr:`~AuditLogDiff.prompts` + - :attr:`~AuditLogDiff.mode` .. versionadded:: 2.6 @@ -4632,7 +4634,7 @@ AuditLogDiff The trigger for the automod rule. - .. note :: + .. note:: The :attr:`~AutoModTrigger.type` of the trigger may be incorrect. Some attributes such as :attr:`~AutoModTrigger.keyword_filter`, :attr:`~AutoModTrigger.regex_patterns`, @@ -4644,7 +4646,7 @@ AuditLogDiff The actions to take when an automod rule is triggered. - :type: List[AutoModRuleAction] + :type: List[:class:`AutoModRuleAction`] .. attribute:: exempt_roles @@ -4798,61 +4800,6 @@ AuditLogDiff :type: :class:`bool` - .. 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:`OnboardingPrompt.in_onboarding` - - :type: :class:`bool` .. this is currently missing the following keys: reason and application_id I'm not sure how to port these