diff --git a/discord/__init__.py b/discord/__init__.py index 48fe10925..c13675e34 100644 --- a/discord/__init__.py +++ b/discord/__init__.py @@ -73,6 +73,7 @@ from .poll import * from .soundboard import * from .subscription import * from .presences import * +from .onboarding import * class VersionInfo(NamedTuple): diff --git a/discord/audit_logs.py b/discord/audit_logs.py index b781dcf80..aceaf367d 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', @@ -73,6 +74,7 @@ if TYPE_CHECKING: from .types.snowflake import Snowflake from .types.command import ApplicationCommandPermissions from .types.automod import AutoModerationAction + from .types.onboarding import Prompt as PromptPayload, PromptOption as PromptOptionPayload from .user import User from .app_commands import AppCommand from .webhook import Webhook @@ -246,6 +248,16 @@ def _transform_default_emoji(entry: AuditLogEntry, data: str) -> PartialEmoji: return PartialEmoji(name=data) +def _transform_onboarding_prompts(entry: AuditLogEntry, data: List[PromptPayload]) -> List[OnboardingPrompt]: + 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.from_dict(data=option, state=entry._state, guild=entry.guild) for option in data] + + E = TypeVar('E', bound=enums.Enum) @@ -268,13 +280,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_prompt_'): + return enums.try_enum(enums.OnboardingPromptType, data) else: return enums.try_enum(enums.ChannelType, data) @@ -353,7 +367,11 @@ class AuditLogChanges: 'flags': (None, _transform_overloaded_flags), 'default_reaction_emoji': (None, _transform_default_reaction), 'emoji_name': ('emoji', _transform_default_emoji), - 'user_id': ('user', _transform_member_id) + 'user_id': ('user', _transform_member_id), + '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/enums.py b/discord/enums.py index acc780120..37d82e796 100644 --- a/discord/enums.py +++ b/discord/enums.py @@ -78,6 +78,8 @@ __all__ = ( 'VoiceChannelEffectAnimationType', 'SubscriptionStatus', 'MessageReferenceType', + 'OnboardingPromptType', + 'OnboardingMode', ) @@ -400,6 +402,13 @@ class AuditLogAction(Enum): automod_timeout_member = 145 creator_monetization_request_created = 150 creator_monetization_terms_accepted = 151 + onboarding_prompt_create = 163 + onboarding_prompt_update = 164 + onboarding_prompt_delete = 165 + onboarding_create = 166 + onboarding_update = 167 + home_settings_create = 190 + home_settings_update = 191 # fmt: on @property @@ -465,6 +474,13 @@ class AuditLogAction(Enum): AuditLogAction.soundboard_sound_create: AuditLogActionCategory.create, AuditLogAction.soundboard_sound_update: AuditLogActionCategory.update, AuditLogAction.soundboard_sound_delete: AuditLogActionCategory.delete, + AuditLogAction.onboarding_prompt_create: AuditLogActionCategory.create, + AuditLogAction.onboarding_prompt_update: AuditLogActionCategory.update, + AuditLogAction.onboarding_prompt_delete: AuditLogActionCategory.delete, + AuditLogAction.onboarding_create: AuditLogActionCategory.create, + AuditLogAction.onboarding_update: AuditLogActionCategory.update, + AuditLogAction.home_settings_create: AuditLogActionCategory.create, + AuditLogAction.home_settings_update: AuditLogActionCategory.update, } # fmt: on return lookup[self] @@ -510,6 +526,12 @@ class AuditLogAction(Enum): return 'user' elif v < 152: return 'creator_monetization' + elif v < 166: + return 'onboarding_prompt' + elif v < 168: + return 'onboarding' + elif v < 192: + return 'home_settings' class UserFlags(Enum): @@ -912,6 +934,16 @@ class SubscriptionStatus(Enum): inactive = 2 +class OnboardingPromptType(Enum): + multiple_choice = 0 + 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 b03dbbea6..5daded9db 100644 --- a/discord/guild.py +++ b/discord/guild.py @@ -76,6 +76,7 @@ from .enums import ( AutoModRuleEventType, ForumOrderType, ForumLayoutType, + OnboardingMode, ) from .mixins import Hashable from .user import User @@ -91,6 +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 from .welcome_screen import WelcomeScreen, WelcomeChannel from .automod import AutoModRule, AutoModTrigger, AutoModRuleAction from .partial_emoji import _EmojiTag, PartialEmoji @@ -139,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] @@ -4867,3 +4870,74 @@ class Guild(Hashable): data = await self._state.http.create_soundboard_sound(self.id, reason=reason, **payload) return SoundboardSound(guild=self, state=self._state, data=data) + + async def onboarding(self) -> Onboarding: + """|coro| + + Fetches the onboarding configuration for this guild. + + .. versionadded:: 2.6 + + 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) + + async def edit_onboarding( + self, + *, + prompts: List[OnboardingPrompt] = MISSING, + default_channels: List[Snowflake] = MISSING, + enabled: bool = MISSING, + mode: OnboardingMode = MISSING, + reason: str = MISSING, + ) -> Onboarding: + """|coro| + + Edits the onboarding configuration for this guild. + + You must have :attr:`Permissions.manage_guild` and + :attr:`Permissions.manage_roles` to do this. + + .. versionadded:: 2.6 + + Parameters + ----------- + prompts: List[:class:`OnboardingPrompt`] + The prompts that will be shown to new members. + This overrides the existing prompts and its options. + default_channels: List[:class:`abc.Snowflake`] + The channels that will be used as the default channels for new members. + This overrides the existing default channels. + enabled: :class:`bool` + Whether the onboarding configuration is enabled. + This overrides the existing enabled state. + 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.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, + 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 02fd1e136..15de396d0 100644 --- a/discord/http.py +++ b/discord/http.py @@ -81,6 +81,7 @@ if TYPE_CHECKING: invite, member, message, + onboarding, template, role, user, @@ -2541,6 +2542,42 @@ class HTTPClient: ), ) + # Guild Onboarding + + 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 edit_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, + ) + # Soundboard def get_soundboard_default_sounds(self) -> Response[List[soundboard.SoundboardDefaultSound]]: diff --git a/discord/onboarding.py b/discord/onboarding.py new file mode 100644 index 000000000..d26258c16 --- /dev/null +++ b/discord/onboarding.py @@ -0,0 +1,369 @@ +""" +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, Iterable, Optional, Set, List, Union + +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', + 'OnboardingPrompt', + 'OnboardingPromptOption', +) + + +if TYPE_CHECKING: + from typing_extensions import Self + + from .abc import GuildChannel, Snowflake + from .emoji import Emoji + from .guild import Guild + from .partial_emoji import PartialEmoji + from .role import Role + from .threads import Thread + from .types.onboarding import ( + Prompt as PromptPayload, + PromptOption as PromptOptionPayload, + CreatePromptOption as CreatePromptOptionPayload, + Onboarding as OnboardingPayload, + ) + from .state import ConnectionState + + +class OnboardingPromptOption(Hashable): + """Represents a onboarding prompt option. + + This can be manually created for :meth:`Guild.edit_onboarding`. + + .. versionadded:: 2.6 + + Parameters + ----------- + title: :class:`str` + The title 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. I + f this is a string, it will be converted to a :class:`PartialEmoji`. + description: Optional[:class:`str`] + The description of this prompt option. + channels: Iterable[Union[:class:`abc.Snowflake`, :class:`int`]] + The channels the user will be added to if this option is selected. + roles: Iterable[Union[:class:`abc.Snowflake`, :class:`int`]] + The roles the user will be given if this option is selected. + + 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`] + The description of this prompt option. + emoji: Optional[Union[:class:`Emoji`, :class:`PartialEmoji`]] + 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 the user will be added to if this option is selected. + role_ids: Set[:class:`int`] + 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, + 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: Optional[Union[Emoji, PartialEmoji]] = ( + PartialEmoji.from_str(emoji) if isinstance(emoji, str) else emoji if emoji is not MISSING else None + ) + + 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 + + def __repr__(self) -> str: + return f'' + + @classmethod + def from_dict(cls, *, data: PromptOptionPayload, state: ConnectionState, guild: Guild) -> Self: + instance = cls( + title=data['title'], + description=data['description'], + 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 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 the prompt option was created manually. + """ + if self._guild is None: + raise ValueError('This prompt does not have an associated guild because it was created manually.') + 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. + + 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. + + 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) + + +class OnboardingPrompt: + """Represents a onboarding prompt. + + This can be manually created for :meth:`Guild.edit_onboarding`. + + .. versionadded:: 2.6 + + Parameters + ----------- + type: :class:`OnboardingPromptType` + The type of this prompt. + title: :class:`str` + The title of this prompt. + options: List[:class:`OnboardingPromptOption`] + The options of this prompt. + single_select: :class:`bool` + Whether this prompt is single select. + Defaults to ``True``. + required: :class:`bool` + Whether this prompt is required. + Defaults to ``True``. + in_onboarding: :class:`bool` + Whether this prompt is in the onboarding flow. + Defaults to ``True``. + + 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` + The title of this prompt. + options: List[:class:`OnboardingPromptOption`] + 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__ = ( + 'id', + 'type', + 'title', + 'options', + 'single_select', + 'required', + 'in_onboarding', + '_guild', + ) + + def __init__( + self, + *, + type: OnboardingPromptType, + title: str, + options: List[OnboardingPromptOption], + single_select: bool = True, + required: bool = True, + in_onboarding: bool = True, + ) -> None: + self.id: int = 0 + self.type: OnboardingPromptType = type + self.title: str = title + 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, + '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, + } + + @property + def guild(self) -> Guild: + """:class:`Guild`: The guild this prompt is related to. + + Raises + ------ + ValueError + If the prompt was created manually. + """ + if self._guild is None: + raise ValueError('This prompt does not have an associated guild because it was created manually.') + return self._guild + + 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: + """Represents a guild's onboarding configuration. + + .. versionadded:: 2.6 + + 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. + mode: :class:`OnboardingMode` + The mode of onboarding for this guild. + """ + + __slots__ = ( + '_state', + '_cs_default_channels', + 'guild', + 'prompts', + 'default_channel_ids', + 'enabled', + 'mode', + ) + + 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.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)) + + 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) + + 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/reaction.py b/discord/reaction.py index 9fd933b0a..060447e13 100644 --- a/discord/reaction.py +++ b/discord/reaction.py @@ -102,7 +102,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'] details = data.get('count_details', {}) diff --git a/discord/state.py b/discord/state.py index 0fbeadea2..097f5b353 100644 --- a/discord/state.py +++ b/discord/state.py @@ -1786,7 +1786,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/types/audit_log.py b/discord/types/audit_log.py index c9d305695..6a7a8b4ab 100644 --- a/discord/types/audit_log.py +++ b/discord/types/audit_log.py @@ -38,6 +38,7 @@ from .channel import ChannelType, DefaultReaction, PrivacyLevel, VideoQualityMod from .threads import Thread from .command import ApplicationCommand, ApplicationCommandPermissions from .automod import AutoModerationTriggerMetadata +from .onboarding import PromptOption, Prompt AuditLogEvent = Literal[ 1, @@ -99,6 +100,13 @@ AuditLogEvent = Literal[ 145, 150, 151, + 163, + 164, + 165, + 166, + 167, + 190, + 191, ] @@ -116,6 +124,7 @@ class _AuditLogChange_Str(TypedDict): 'tags', 'unicode_emoji', 'emoji_name', + 'title', ] new_value: str old_value: str @@ -163,6 +172,10 @@ class _AuditLogChange_Bool(TypedDict): 'available', 'archived', 'locked', + 'enabled', + 'single_select', + 'required', + 'in_onboarding', ] new_value: bool old_value: bool @@ -273,8 +286,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] @@ -297,6 +310,18 @@ class _AuditLogChange_TriggerMetadata(TypedDict): old_value: Optional[AutoModerationTriggerMetadata] +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] + + class _AuditLogChange_RoleColours(TypedDict): key: Literal['colors'] new_value: RoleColours @@ -323,10 +348,12 @@ AuditLogChange = Union[ _AuditLogChange_Status, _AuditLogChange_EntityType, _AuditLogChange_AppCommandPermissions, - _AuditLogChange_AppliedTags, + _AuditLogChange_SnowflakeList, _AuditLogChange_AvailableTags, _AuditLogChange_DefaultReactionEmoji, _AuditLogChange_TriggerMetadata, + _AuditLogChange_Prompts, + _AuditLogChange_Options, _AuditLogChange_RoleColours, ] diff --git a/discord/types/onboarding.py b/discord/types/onboarding.py new file mode 100644 index 000000000..64f9c45c8 --- /dev/null +++ b/discord/types/onboarding.py @@ -0,0 +1,72 @@ +""" +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, 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): + channel_ids: List[Snowflake] + role_ids: List[Snowflake] + 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[Union[PromptOption, CreatePromptOption]] + 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 + mode: OnboardingMode diff --git a/discord/webhook/async_.py b/discord/webhook/async_.py index 3b62b10fa..665d704ab 100644 --- a/discord/webhook/async_.py +++ b/discord/webhook/async_.py @@ -755,7 +755,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') diff --git a/docs/api.rst b/docs/api.rst index c7d9e351f..75785b5bf 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -3103,6 +3103,89 @@ of :class:`enum.Enum`. .. versionadded:: 2.5 + .. attribute:: onboarding_prompt_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.6 + + .. attribute:: onboarding_prompt_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.6 + + .. attribute:: onboarding_prompt_delete + + A guild onboarding prompt was deleted. + + 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.6 + + .. attribute:: onboarding_create + + The guild's onboarding configuration was created. + + Possible attributes for :class:`AuditLogDiff`: + + - :attr:`~AuditLogDiff.enabled` + - :attr:`~AuditLogDiff.default_channels` + - :attr:`~AuditLogDiff.prompts` + - :attr:`~AuditLogDiff.mode` + + .. versionadded:: 2.6 + + .. 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` + - :attr:`~AuditLogDiff.mode` + + .. versionadded:: 2.6 + + .. attribute:: home_settings_create + + The guild's server guide was created. + + .. versionadded:: 2.6 + + .. attribute:: home_settings_update + + The guild's server guide was updated. + + .. versionadded:: 2.6 + .. class:: AuditLogActionCategory Represents the category that the :class:`AuditLogAction` belongs to. @@ -3881,6 +3964,35 @@ of :class:`enum.Enum`. An alias for :attr:`.default`. +.. class:: OnboardingPromptType + + Represents the type of onboarding prompt. + + .. versionadded:: 2.6 + + .. attribute:: multiple_choice + + Prompt options are multiple choice. + + .. attribute:: dropdown + + Prompt options are displayed as a drop-down. + +.. class:: OnboardingMode + + Represents the onboarding constraint mode. + + .. versionadded:: 2.6 + + .. attribute:: default + + Only default channels count towards onboarding constraints. + + .. attribute:: advanced + + Default channels and questions count towards onboarding constraints. + + .. _discord-api-audit-logs: Audit Log Data @@ -4127,9 +4239,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 @@ -4502,7 +4614,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` @@ -4522,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`, @@ -4534,7 +4646,7 @@ AuditLogDiff The actions to take when an automod rule is triggered. - :type: List[AutoModRuleAction] + :type: List[:class:`AutoModRuleAction`] .. attribute:: exempt_roles @@ -4632,6 +4744,71 @@ AuditLogDiff :type: :class:`float` + .. 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` + + .. attribute:: mode + + The onboarding constraint mode. + + See also :attr:`Onboarding.mode` + + :type: :class:`OnboardingMode` + + .. this is currently missing the following keys: reason and application_id I'm not sure how to port these @@ -5255,6 +5432,31 @@ GuildSticker .. autoclass:: GuildSticker() :members: +Onboarding +~~~~~~~~~~~ + +.. attributetable:: Onboarding + +.. autoclass:: Onboarding() + :members: + +OnboardingPrompt +~~~~~~~~~~~~~~~~~ + +.. attributetable:: OnboardingPrompt + +.. autoclass:: OnboardingPrompt() + :members: + + +OnboardingPromptOption +~~~~~~~~~~~~~~~~~~~~~~~ + +.. attributetable:: OnboardingPromptOption + +.. autoclass:: OnboardingPromptOption() + :members: + BaseSoundboardSound ~~~~~~~~~~~~~~~~~~~~~~~