diff --git a/interactions/__init__.py b/interactions/__init__.py index e4b9cce38..292e71f38 100644 --- a/interactions/__init__.py +++ b/interactions/__init__.py @@ -54,8 +54,8 @@ ActivityTimestamps, ActivityType, AllowedMentions, - Application, application_commands_to_dict, + Application, ApplicationCommandPermission, ApplicationFlags, Asset, @@ -99,8 +99,8 @@ ChannelType, check, ClientUser, - Color, COLOR_TYPES, + Color, Colour, CommandType, component_callback, @@ -108,10 +108,11 @@ ComponentContext, ComponentType, ConsumeRest, - contexts, + ContainerComponent, context_menu, ContextMenu, ContextMenuContext, + contexts, ContextType, Converter, cooldown, @@ -123,8 +124,8 @@ DateTrigger, DefaultNotificationLevel, DefaultReaction, - DM, dm_only, + DM, DMChannel, DMChannelConverter, DMConverter, @@ -138,17 +139,20 @@ EmbedProvider, Entitlement, ExplicitContentFilterLevel, + ExponentialBackoffSystem, Extension, File, + FileComponent, FlatUIColors, FlatUIColours, ForumLayoutType, + ForumSortOrder, get_components_ids, global_autocomplete, GlobalAutoComplete, Greedy, - Guild, guild_only, + Guild, GuildBan, GuildCategory, GuildCategoryConverter, @@ -184,9 +188,9 @@ has_role, IDConverter, InputText, + integration_types, IntegrationExpireBehaviour, IntegrationType, - integration_types, Intents, InteractionCommand, InteractionContext, @@ -198,6 +202,7 @@ Invite, InviteTargetType, is_owner, + LeakyBucketSystem, listen, Listener, LocalisedDesc, @@ -208,13 +213,15 @@ MaterialColours, max_concurrency, MaxConcurrency, + MediaGalleryComponent, + MediaGalleryItem, Member, MemberConverter, MemberFlags, MentionableSelectMenu, MentionType, - Message, message_context_menu, + Message, MessageableChannelConverter, MessageableMixin, MessageActivity, @@ -226,19 +233,19 @@ MessageReference, MessageType, MFALevel, - Modal, modal_callback, + Modal, ModalCommand, ModalContext, MODEL_TO_CONVERTER, NoArgumentConverter, NSFWLevel, - open_file, Onboarding, OnboardingMode, OnboardingPrompt, OnboardingPromptOption, OnboardingPromptType, + open_file, OptionType, OrTrigger, OverwriteType, @@ -261,8 +268,8 @@ process_components, process_default_reaction, process_embeds, - process_emoji, process_emoji_req_format, + process_emoji, process_message_payload, process_message_reference, process_permission_overwrites, @@ -279,6 +286,9 @@ ScheduledEventPrivacyLevel, ScheduledEventStatus, ScheduledEventType, + SectionComponent, + SeparatorComponent, + SeparatorSpacingSize, ShortText, slash_attachment_option, slash_bool_option, @@ -297,8 +307,9 @@ SlashCommandOption, SlashCommandParameter, SlashContext, - Snowflake, + SlidingWindowSystem, Snowflake_Type, + Snowflake, SnowflakeConverter, SnowflakeObject, spread_to_rows, @@ -319,6 +330,7 @@ Team, TeamMember, TeamMembershipState, + TextDisplayComponent, TextStyles, ThreadableMixin, ThreadChannel, @@ -326,12 +338,14 @@ ThreadList, ThreadMember, ThreadTag, + ThumbnailComponent, Timestamp, TimestampStyles, TimeTrigger, to_optional_snowflake, - to_snowflake, to_snowflake_list, + to_snowflake, + TokenBucketSystem, TYPE_ALL_ACTION, TYPE_ALL_CHANNEL, TYPE_ALL_TRIGGER, @@ -343,9 +357,11 @@ TYPE_THREAD_CHANNEL, TYPE_VOICE_CHANNEL, Typing, + UnfurledMediaItem, + UnfurledMediaItemLoadingState, UPLOADABLE_TYPE, - User, user_context_menu, + User, UserConverter, UserFlags, UserSelectMenu, @@ -359,11 +375,6 @@ WebhookMixin, WebhookTypes, WebSocketOPCode, - SlidingWindowSystem, - ExponentialBackoffSystem, - LeakyBucketSystem, - TokenBucketSystem, - ForumSortOrder, ) from .api import events from . import ext @@ -385,8 +396,8 @@ "ActivityTimestamps", "ActivityType", "AllowedMentions", - "Application", "application_commands_to_dict", + "Application", "ApplicationCommandPermission", "ApplicationFlags", "Asset", @@ -433,50 +444,47 @@ "Client", "ClientT", "ClientUser", - "Color", "COLOR_TYPES", + "Color", "Colour", "CommandType", "component_callback", "ComponentCommand", "ComponentContext", "ComponentType", - "ConsumeRest", "const", - "contexts", - "context_menu", + "ConsumeRest", + "ContainerComponent", "CONTEXT_MENU_NAME_LENGTH", + "context_menu", "ContextMenu", "ContextMenuContext", + "contexts", "ContextType", "Converter", "cooldown", "Cooldown", "CooldownSystem", "CronTrigger", - "SlidingWindowSystem", - "ExponentialBackoffSystem", - "LeakyBucketSystem", - "TokenBucketSystem", "CustomEmoji", "CustomEmojiConverter", "DateTrigger", "DefaultNotificationLevel", "DefaultReaction", "DISCORD_EPOCH", - "DM", "dm_only", + "DM", "DMChannel", "DMChannelConverter", "DMConverter", "DMGroup", "DMGroupConverter", - "Embed", "EMBED_FIELD_VALUE_LENGTH", "EMBED_MAX_DESC_LENGTH", "EMBED_MAX_FIELDS", "EMBED_MAX_NAME_LENGTH", "EMBED_TOTAL_MAX", + "Embed", "EmbedAttachment", "EmbedAuthor", "EmbedField", @@ -486,13 +494,15 @@ "errors", "events", "ExplicitContentFilterLevel", + "ExponentialBackoffSystem", "ext", "Extension", "File", + "FileComponent", "FlatUIColors", "FlatUIColours", - "ForumSortOrder", "ForumLayoutType", + "ForumSortOrder", "get_components_ids", "get_logger", "global_autocomplete", @@ -500,8 +510,8 @@ "GlobalAutoComplete", "GlobalScope", "Greedy", - "Guild", "guild_only", + "Guild", "GuildBan", "GuildCategory", "GuildCategoryConverter", @@ -537,9 +547,9 @@ "has_role", "IDConverter", "InputText", + "integration_types", "IntegrationExpireBehaviour", "IntegrationType", - "integration_types", "Intents", "InteractionCommand", "InteractionContext", @@ -552,6 +562,7 @@ "InviteTargetType", "is_owner", "kwarg_spam", + "LeakyBucketSystem", "listen", "Listener", "LocalisedDesc", @@ -563,6 +574,8 @@ "MaterialColours", "max_concurrency", "MaxConcurrency", + "MediaGalleryComponent", + "MediaGalleryItem", "Member", "MemberConverter", "MemberFlags", @@ -570,8 +583,8 @@ "MentionableSelectMenu", "MentionPrefix", "MentionType", - "Message", "message_context_menu", + "Message", "MessageableChannelConverter", "MessageableMixin", "MessageActivity", @@ -585,19 +598,19 @@ "MFALevel", "Missing", "MISSING", - "Modal", "modal_callback", + "Modal", "ModalCommand", "ModalContext", "MODEL_TO_CONVERTER", "NoArgumentConverter", "NSFWLevel", - "open_file", "Onboarding", "OnboardingMode", "OnboardingPrompt", "OnboardingPromptOption", "OnboardingPromptType", + "open_file", "OptionType", "OrTrigger", "OverwriteType", @@ -606,12 +619,12 @@ "PartialEmojiConverter", "PermissionOverwrite", "Permissions", + "POLL_MAX_ANSWERS", + "POLL_MAX_DURATION_HOURS", "Poll", "PollAnswer", "PollAnswerCount", "PollLayoutType", - "POLL_MAX_ANSWERS", - "POLL_MAX_DURATION_HOURS", "PollMedia", "PollResults", "PREMIUM_GUILD_LIMITS", @@ -623,8 +636,8 @@ "process_components", "process_default_reaction", "process_embeds", - "process_emoji", "process_emoji_req_format", + "process_emoji", "process_message_payload", "process_message_reference", "process_permission_overwrites", @@ -641,9 +654,12 @@ "ScheduledEventPrivacyLevel", "ScheduledEventStatus", "ScheduledEventType", + "SectionComponent", "SELECT_MAX_NAME_LENGTH", "SELECTS_MAX_OPTIONS", "Sentinel", + "SeparatorComponent", + "SeparatorSpacingSize", "ShortText", "Singleton", "slash_attachment_option", @@ -657,8 +673,8 @@ "slash_float_option", "slash_int_option", "slash_mentionable_option", - "slash_option", "SLASH_OPTION_NAME_LENGTH", + "slash_option", "slash_role_option", "slash_str_option", "slash_user_option", @@ -667,9 +683,10 @@ "SlashCommandOption", "SlashCommandParameter", "SlashContext", + "SlidingWindowSystem", "smart_cache", - "Snowflake", "Snowflake_Type", + "Snowflake", "SnowflakeConverter", "SnowflakeObject", "spread_to_rows", @@ -686,12 +703,13 @@ "subcommand", "sync_needed", "SystemChannelFlags", - "T", "T_co", + "T", "Task", "Team", "TeamMember", "TeamMembershipState", + "TextDisplayComponent", "TextStyles", "ThreadableMixin", "ThreadChannel", @@ -699,12 +717,14 @@ "ThreadList", "ThreadMember", "ThreadTag", + "ThumbnailComponent", "Timestamp", "TimestampStyles", "TimeTrigger", "to_optional_snowflake", - "to_snowflake", "to_snowflake_list", + "to_snowflake", + "TokenBucketSystem", "TYPE_ALL_ACTION", "TYPE_ALL_CHANNEL", "TYPE_ALL_TRIGGER", @@ -716,9 +736,11 @@ "TYPE_THREAD_CHANNEL", "TYPE_VOICE_CHANNEL", "Typing", + "UnfurledMediaItem", + "UnfurledMediaItemLoadingState", "UPLOADABLE_TYPE", - "User", "user_context_menu", + "User", "UserConverter", "UserFlags", "UserSelectMenu", diff --git a/interactions/models/__init__.py b/interactions/models/__init__.py index c258996e4..fb8c6d74c 100644 --- a/interactions/models/__init__.py +++ b/interactions/models/__init__.py @@ -38,11 +38,12 @@ ChannelSelectMenu, ChannelType, ClientUser, - Color, COLOR_TYPES, + Color, Colour, CommandType, ComponentType, + ContainerComponent, ContextType, CustomEmoji, DefaultNotificationLevel, @@ -59,9 +60,11 @@ Entitlement, ExplicitContentFilterLevel, File, + FileComponent, FlatUIColors, FlatUIColours, ForumLayoutType, + ForumSortOrder, get_components_ids, Guild, GuildBan, @@ -96,6 +99,8 @@ InviteTargetType, MaterialColors, MaterialColours, + MediaGalleryComponent, + MediaGalleryItem, Member, MemberFlags, MentionableSelectMenu, @@ -112,13 +117,13 @@ MFALevel, Modal, NSFWLevel, - open_file, - OverwriteType, Onboarding, OnboardingMode, OnboardingPrompt, OnboardingPromptOption, OnboardingPromptType, + open_file, + OverwriteType, ParagraphText, PartialEmoji, PermissionOverwrite, @@ -137,8 +142,8 @@ process_components, process_default_reaction, process_embeds, - process_emoji, process_emoji_req_format, + process_emoji, process_message_payload, process_message_reference, process_permission_overwrites, @@ -153,9 +158,12 @@ ScheduledEventPrivacyLevel, ScheduledEventStatus, ScheduledEventType, + SectionComponent, + SeparatorComponent, + SeparatorSpacingSize, ShortText, - Snowflake, Snowflake_Type, + Snowflake, SnowflakeObject, spread_to_rows, StageInstance, @@ -172,17 +180,19 @@ Team, TeamMember, TeamMembershipState, + TextDisplayComponent, TextStyles, ThreadableMixin, ThreadChannel, ThreadList, ThreadMember, ThreadTag, + ThumbnailComponent, Timestamp, TimestampStyles, to_optional_snowflake, - to_snowflake, to_snowflake_list, + to_snowflake, TYPE_ALL_ACTION, TYPE_ALL_CHANNEL, TYPE_ALL_TRIGGER, @@ -193,6 +203,8 @@ TYPE_MESSAGEABLE_CHANNEL, TYPE_THREAD_CHANNEL, TYPE_VOICE_CHANNEL, + UnfurledMediaItem, + UnfurledMediaItemLoadingState, UPLOADABLE_TYPE, User, UserFlags, @@ -205,7 +217,6 @@ WebhookMixin, WebhookTypes, WebSocketOPCode, - ForumSortOrder, ) from .internal import ( ActiveVoiceState, @@ -333,8 +344,8 @@ "ActivityTimestamps", "ActivityType", "AllowedMentions", - "Application", "application_commands_to_dict", + "Application", "ApplicationCommandPermission", "ApplicationFlags", "Asset", @@ -378,8 +389,8 @@ "ChannelType", "check", "ClientUser", - "Color", "COLOR_TYPES", + "Color", "Colour", "CommandType", "component_callback", @@ -387,27 +398,24 @@ "ComponentContext", "ComponentType", "ConsumeRest", - "contexts", + "ContainerComponent", "context_menu", "ContextMenu", "ContextMenuContext", + "contexts", "ContextType", "Converter", "cooldown", "Cooldown", "CooldownSystem", "CronTrigger", - "SlidingWindowSystem", - "ExponentialBackoffSystem", - "LeakyBucketSystem", - "TokenBucketSystem", "CustomEmoji", "CustomEmojiConverter", "DateTrigger", "DefaultNotificationLevel", "DefaultReaction", - "DM", "dm_only", + "DM", "DMChannel", "DMChannelConverter", "DMConverter", @@ -421,18 +429,20 @@ "EmbedProvider", "Entitlement", "ExplicitContentFilterLevel", + "ExponentialBackoffSystem", "Extension", "File", + "FileComponent", "FlatUIColors", "FlatUIColours", - "ForumSortOrder", "ForumLayoutType", + "ForumSortOrder", "get_components_ids", "global_autocomplete", "GlobalAutoComplete", "Greedy", - "Guild", "guild_only", + "Guild", "GuildBan", "GuildCategory", "GuildCategoryConverter", @@ -468,9 +478,9 @@ "has_role", "IDConverter", "InputText", + "integration_types", "IntegrationExpireBehaviour", "IntegrationType", - "integration_types", "Intents", "InteractionCommand", "InteractionContext", @@ -482,6 +492,7 @@ "Invite", "InviteTargetType", "is_owner", + "LeakyBucketSystem", "listen", "Listener", "LocalisedDesc", @@ -492,13 +503,15 @@ "MaterialColours", "max_concurrency", "MaxConcurrency", + "MediaGalleryComponent", + "MediaGalleryItem", "Member", "MemberConverter", "MemberFlags", "MentionableSelectMenu", "MentionType", - "Message", "message_context_menu", + "Message", "MessageableChannelConverter", "MessageableMixin", "MessageActivity", @@ -510,19 +523,19 @@ "MessageReference", "MessageType", "MFALevel", - "Modal", "modal_callback", + "Modal", "ModalCommand", "ModalContext", "MODEL_TO_CONVERTER", "NoArgumentConverter", "NSFWLevel", - "open_file", "Onboarding", "OnboardingMode", "OnboardingPrompt", "OnboardingPromptOption", "OnboardingPromptType", + "open_file", "OptionType", "OrTrigger", "OverwriteType", @@ -545,8 +558,8 @@ "process_components", "process_default_reaction", "process_embeds", - "process_emoji", "process_emoji_req_format", + "process_emoji", "process_message_payload", "process_message_reference", "process_permission_overwrites", @@ -563,6 +576,9 @@ "ScheduledEventPrivacyLevel", "ScheduledEventStatus", "ScheduledEventType", + "SectionComponent", + "SeparatorComponent", + "SeparatorSpacingSize", "ShortText", "slash_attachment_option", "slash_bool_option", @@ -581,8 +597,9 @@ "SlashCommandOption", "SlashCommandParameter", "SlashContext", - "Snowflake", + "SlidingWindowSystem", "Snowflake_Type", + "Snowflake", "SnowflakeConverter", "SnowflakeObject", "spread_to_rows", @@ -603,6 +620,7 @@ "Team", "TeamMember", "TeamMembershipState", + "TextDisplayComponent", "TextStyles", "ThreadableMixin", "ThreadChannel", @@ -610,12 +628,14 @@ "ThreadList", "ThreadMember", "ThreadTag", + "ThumbnailComponent", "Timestamp", "TimestampStyles", "TimeTrigger", "to_optional_snowflake", - "to_snowflake", "to_snowflake_list", + "to_snowflake", + "TokenBucketSystem", "TYPE_ALL_ACTION", "TYPE_ALL_CHANNEL", "TYPE_ALL_TRIGGER", @@ -627,9 +647,11 @@ "TYPE_THREAD_CHANNEL", "TYPE_VOICE_CHANNEL", "Typing", + "UnfurledMediaItem", + "UnfurledMediaItemLoadingState", "UPLOADABLE_TYPE", - "User", "user_context_menu", + "User", "UserConverter", "UserFlags", "UserSelectMenu", diff --git a/interactions/models/discord/__init__.py b/interactions/models/discord/__init__.py index 21baa4d68..667024caf 100644 --- a/interactions/models/discord/__init__.py +++ b/interactions/models/discord/__init__.py @@ -57,15 +57,24 @@ BaseSelectMenu, Button, ChannelSelectMenu, + ContainerComponent, + FileComponent, get_components_ids, InteractiveComponent, + MediaGalleryComponent, + MediaGalleryItem, MentionableSelectMenu, process_components, RoleSelectMenu, + SectionComponent, + SeparatorComponent, spread_to_rows, StringSelectMenu, StringSelectOption, + TextDisplayComponent, + ThumbnailComponent, TYPE_COMPONENT_MAPPING, + UnfurledMediaItem, UserSelectMenu, ) @@ -87,6 +96,7 @@ DefaultNotificationLevel, ExplicitContentFilterLevel, ForumLayoutType, + ForumSortOrder, IntegrationExpireBehaviour, IntegrationType, Intents, @@ -110,17 +120,18 @@ ScheduledEventPrivacyLevel, ScheduledEventStatus, ScheduledEventType, + SeparatorSpacingSize, StagePrivacyLevel, Status, StickerFormatType, StickerTypes, SystemChannelFlags, TeamMembershipState, + UnfurledMediaItemLoadingState, UserFlags, VerificationLevel, VideoQualityMode, WebSocketOPCode, - ForumSortOrder, ) from .file import File, open_file, UPLOADABLE_TYPE from .guild import ( @@ -217,11 +228,12 @@ "ChannelSelectMenu", "ChannelType", "ClientUser", - "Color", "COLOR_TYPES", + "Color", "Colour", "CommandType", "ComponentType", + "ContainerComponent", "ContextType", "CustomEmoji", "DefaultNotificationLevel", @@ -238,10 +250,11 @@ "Entitlement", "ExplicitContentFilterLevel", "File", + "FileComponent", "FlatUIColors", "FlatUIColours", - "ForumSortOrder", "ForumLayoutType", + "ForumSortOrder", "get_components_ids", "Guild", "GuildBan", @@ -276,6 +289,8 @@ "InviteTargetType", "MaterialColors", "MaterialColours", + "MediaGalleryComponent", + "MediaGalleryItem", "Member", "MemberFlags", "MentionableSelectMenu", @@ -292,12 +307,12 @@ "MFALevel", "Modal", "NSFWLevel", - "open_file", "Onboarding", "OnboardingMode", "OnboardingPrompt", "OnboardingPromptOption", "OnboardingPromptType", + "open_file", "OverwriteType", "ParagraphText", "PartialEmoji", @@ -317,8 +332,8 @@ "process_components", "process_default_reaction", "process_embeds", - "process_emoji", "process_emoji_req_format", + "process_emoji", "process_message_payload", "process_message_reference", "process_permission_overwrites", @@ -333,9 +348,12 @@ "ScheduledEventPrivacyLevel", "ScheduledEventStatus", "ScheduledEventType", + "SectionComponent", + "SeparatorComponent", + "SeparatorSpacingSize", "ShortText", - "Snowflake", "Snowflake_Type", + "Snowflake", "SnowflakeObject", "spread_to_rows", "StageInstance", @@ -352,17 +370,19 @@ "Team", "TeamMember", "TeamMembershipState", + "TextDisplayComponent", "TextStyles", "ThreadableMixin", "ThreadChannel", "ThreadList", "ThreadMember", "ThreadTag", + "ThumbnailComponent", "Timestamp", "TimestampStyles", "to_optional_snowflake", - "to_snowflake", "to_snowflake_list", + "to_snowflake", "TYPE_ALL_ACTION", "TYPE_ALL_CHANNEL", "TYPE_ALL_TRIGGER", @@ -373,6 +393,8 @@ "TYPE_MESSAGEABLE_CHANNEL", "TYPE_THREAD_CHANNEL", "TYPE_VOICE_CHANNEL", + "UnfurledMediaItem", + "UnfurledMediaItemLoadingState", "UPLOADABLE_TYPE", "User", "UserFlags", diff --git a/interactions/models/discord/components.py b/interactions/models/discord/components.py index 495874832..d045fb7ea 100644 --- a/interactions/models/discord/components.py +++ b/interactions/models/discord/components.py @@ -12,32 +12,79 @@ from interactions.client.mixins.serialization import DictSerializationMixin from interactions.models.discord.base import DiscordObject from interactions.models.discord.emoji import PartialEmoji, process_emoji -from interactions.models.discord.enums import ButtonStyle, ChannelType, ComponentType +from interactions.models.discord.enums import ( + ButtonStyle, + ChannelType, + ComponentType, + SeparatorSpacingSize, + UnfurledMediaItemLoadingState, +) if TYPE_CHECKING: import interactions.models.discord __all__ = ( - "BaseComponent", - "InteractiveComponent", "ActionRow", - "Button", + "BaseComponent", "BaseSelectMenu", - "StringSelectMenu", - "StringSelectOption", - "UserSelectMenu", - "RoleSelectMenu", - "MentionableSelectMenu", + "Button", "ChannelSelectMenu", + "ContainerComponent", + "DefaultableSelectMenu", + "FileComponent", + "get_components_ids", + "InteractiveComponent", + "MediaGalleryComponent", + "MediaGalleryItem", + "MentionableSelectMenu", "process_components", + "RoleSelectMenu", + "SectionComponent", + "SelectDefaultValues", + "SeparatorComponent", "spread_to_rows", - "get_components_ids", + "StringSelectMenu", + "StringSelectOption", + "TextDisplayComponent", + "ThumbnailComponent", "TYPE_COMPONENT_MAPPING", - "SelectDefaultValues", - "DefaultableSelectMenu", + "UnfurledMediaItem", + "UserSelectMenu", ) +class UnfurledMediaItem(DictSerializationMixin): + """A basic object for making media items.""" + + url: str + proxy_url: Optional[str] = None + height: Optional[int] = None + width: Optional[int] = None + content_type: Optional[str] = None + loading_state: Optional[UnfurledMediaItemLoadingState] = None + + def __init__(self, url: str): + self.url = url + + @classmethod + def from_dict(cls, data: dict) -> "UnfurledMediaItem": + item = cls(data["url"]) + item.proxy_url = data.get("proxy_url") + item.height = data.get("height") + item.width = data.get("width") + item.content_type = data.get("content_type") + item.loading_state = ( + UnfurledMediaItemLoadingState(data.get("loading_state")) if data.get("loading_state") else None + ) + return item + + def __repr__(self) -> str: + return f"<{self.__class__.__name__} url={self.url}>" + + def to_dict(self) -> Dict[str, Any]: + return {"url": self.url} + + class BaseComponent(DictSerializationMixin): """ A base component class. @@ -48,6 +95,7 @@ class BaseComponent(DictSerializationMixin): """ type: ComponentType + id: Optional[int] = None def __repr__(self) -> str: return f"<{self.__class__.__name__} type={self.type}>" @@ -763,6 +811,227 @@ def to_dict(self) -> discord_typings.SelectMenuComponentData: } +class SectionComponent(BaseComponent): + components: "list[TextDisplayComponent]" + accessory: "Button | ThumbnailComponent" + + def __init__( + self, *, components: "list[TextDisplayComponent] | None" = None, accessory: "Button | ThumbnailComponent" + ): + self.components = components or [] + self.accessory = accessory + self.type = ComponentType.SECTION + + @classmethod + def from_dict(cls, data: dict) -> "SectionComponent": + return cls( + components=[BaseComponent.from_dict_factory(component) for component in data["components"]], accessory=BaseComponent.from_dict_factory(data["accessory"]) # type: ignore + ) + + def __repr__(self) -> str: + return f"<{self.__class__.__name__} type={self.type} components={self.components} accessory={self.accessory}>" + + def to_dict(self) -> dict: + return { + "type": self.type.value, + "components": [c.to_dict() for c in self.components], + "accessory": self.accessory.to_dict(), + } + + +class TextDisplayComponent(BaseComponent): + content: str + + def __init__(self, content: str): + self.content = content + self.type = ComponentType.TEXT_DISPLAY + + @classmethod + def from_dict(cls, data: dict) -> "TextDisplayComponent": + return cls(data["content"]) + + def __repr__(self) -> str: + return f"<{self.__class__.__name__} type={self.type} style={self.content}>" + + def to_dict(self) -> dict: + return { + "type": self.type.value, + "content": self.content, + } + + +class ThumbnailComponent(BaseComponent): + media: UnfurledMediaItem + description: Optional[str] = None + spoiler: bool = False + + def __init__(self, media: UnfurledMediaItem, *, description: Optional[str] = None, spoiler: bool = False): + self.media = media + self.description = description + self.spoiler = spoiler + self.type = ComponentType.THUMBNAIL + + @classmethod + def from_dict(cls, data: dict) -> "ThumbnailComponent": + return cls( + media=UnfurledMediaItem.from_dict(data["media"]), + description=data.get("description"), + spoiler=data.get("spoiler", False), + ) + + def __repr__(self) -> str: + return f"<{self.__class__.__name__} type={self.type} media={self.media} description={self.description} spoiler={self.spoiler}>" + + def to_dict(self) -> dict: + return { + "type": self.type.value, + "media": self.media.to_dict(), + "description": self.description, + "spoiler": self.spoiler, + } + + +class MediaGalleryItem(DictSerializationMixin): + media: UnfurledMediaItem + description: Optional[str] = None + spoiler: bool = False + + def __init__(self, media: UnfurledMediaItem, *, description: Optional[str] = None, spoiler: bool = False): + self.media = media + self.description = description + self.spoiler = spoiler + + @classmethod + def from_dict(cls, data: dict) -> "MediaGalleryItem": + return cls( + media=UnfurledMediaItem.from_dict(data["media"]), + description=data.get("description"), + spoiler=data.get("spoiler", False), + ) + + def __repr__(self) -> str: + return f"<{self.__class__.__name__} media={self.media} description={self.description} spoiler={self.spoiler}>" + + def to_dict(self) -> dict: + return { + "media": self.media.to_dict(), + "description": self.description, + "spoiler": self.spoiler, + } + + +class MediaGalleryComponent(BaseComponent): + items: list[MediaGalleryItem] + + def __init__(self, items: list[MediaGalleryItem] | None = None): + self.items = items or [] + self.type = ComponentType.MEDIA_GALLERY + + @classmethod + def from_dict(cls, data: dict) -> "MediaGalleryComponent": + return cls([MediaGalleryItem.from_dict(item) for item in data["items"]]) + + def __repr__(self) -> str: + return f"<{self.__class__.__name__} type={self.type} items={self.items}>" + + def to_dict(self) -> dict: + return { + "type": self.type.value, + "items": [item.to_dict() for item in self.items], + } + + +class FileComponent(BaseComponent): + file: UnfurledMediaItem + spoiler: bool = False + + def __init__(self, file: UnfurledMediaItem, *, spoiler: bool = False): + self.file = file + self.spoiler = spoiler + self.type = ComponentType.FILE + + @classmethod + def from_dict(cls, data: dict) -> "FileComponent": + return cls(file=UnfurledMediaItem.from_dict(data["file"]), spoiler=data.get("spoiler", False)) + + def __repr__(self) -> str: + return f"<{self.__class__.__name__} type={self.type} file={self.file} spoiler={self.spoiler}>" + + def to_dict(self) -> dict: + return { + "type": self.type.value, + "file": self.file.to_dict(), + "spoiler": self.spoiler, + } + + +class SeparatorComponent(BaseComponent): + divider: bool = False + spacing: SeparatorSpacingSize = SeparatorSpacingSize.SMALL + + def __init__(self, *, divider: bool = False, spacing: SeparatorSpacingSize | int = SeparatorSpacingSize.SMALL): + self.divider = divider + self.spacing = SeparatorSpacingSize(spacing) + self.type = ComponentType.SEPARATOR + + @classmethod + def from_dict(cls, data: dict) -> "SeparatorComponent": + return cls(divider=data.get("divider", False), spacing=data.get("spacing", SeparatorSpacingSize.SMALL)) + + def __repr__(self) -> str: + return f"<{self.__class__.__name__} type={self.type} divider={self.divider} spacing={self.spacing}>" + + def to_dict(self) -> dict: + return { + "type": self.type.value, + "divider": self.divider, + "spacing": self.spacing, + } + + +class ContainerComponent(BaseComponent): + components: list[ + ActionRow | SectionComponent | TextDisplayComponent | MediaGalleryComponent | FileComponent | SeparatorComponent + ] + accent_color: Optional[int] = None + spoiler: bool = False + + def __init__( + self, + *components: ActionRow + | SectionComponent + | TextDisplayComponent + | MediaGalleryComponent + | FileComponent + | SeparatorComponent, + accent_color: Optional[int] = None, + spoiler: bool = False, + ): + self.components = list(components) + self.accent_color = accent_color + self.spoiler = spoiler + self.type = ComponentType.CONTAINER + + @classmethod + def from_dict(cls, data: dict) -> "ContainerComponent": + return cls( + *[BaseComponent.from_dict_factory(component) for component in data["components"]], + accent_color=data.get("accent_color"), + spoiler=data.get("spoiler", False), + ) + + def __repr__(self) -> str: + return f"<{self.__class__.__name__} type={self.type} components={self.components} accent_color={self.accent_color} spoiler={self.spoiler}>" + + def to_dict(self) -> dict: + return { + "type": self.type.value, + "components": [component.to_dict() for component in self.components], + "accent_color": self.accent_color, + "spoiler": self.spoiler, + } + + def process_components( components: Optional[ Union[ @@ -806,6 +1075,7 @@ def process_components( if all(isinstance(c, list) for c in components): # list of lists... actionRow-less sending + # note: we're assuming if someone passes a list of lists, they mean to use v1 components return [ActionRow(*row).to_dict() for row in components] if all(issubclass(type(c), InteractiveComponent) for c in components): @@ -816,6 +1086,9 @@ def process_components( # we have a list of action rows return [action_row.to_dict() for action_row in components] + # assume just a list of components + return [c if isinstance(c, dict) else c.to_dict() for c in components] + raise ValueError(f"Invalid components: {components}") @@ -880,4 +1153,11 @@ def get_components_ids(component: Union[str, dict, list, InteractiveComponent]) ComponentType.CHANNEL_SELECT: ChannelSelectMenu, ComponentType.ROLE_SELECT: RoleSelectMenu, ComponentType.MENTIONABLE_SELECT: MentionableSelectMenu, + ComponentType.SECTION: SectionComponent, + ComponentType.TEXT_DISPLAY: TextDisplayComponent, + ComponentType.THUMBNAIL: ThumbnailComponent, + ComponentType.MEDIA_GALLERY: MediaGalleryComponent, + ComponentType.FILE: FileComponent, + ComponentType.SEPARATOR: SeparatorComponent, + ComponentType.CONTAINER: ContainerComponent, } diff --git a/interactions/models/discord/enums.py b/interactions/models/discord/enums.py index c437a619e..41f081986 100644 --- a/interactions/models/discord/enums.py +++ b/interactions/models/discord/enums.py @@ -44,12 +44,14 @@ "ScheduledEventPrivacyLevel", "ScheduledEventStatus", "ScheduledEventType", + "SeparatorSpacingSize", "StagePrivacyLevel", "Status", "StickerFormatType", "StickerTypes", "SystemChannelFlags", "TeamMembershipState", + "UnfurledMediaItemLoadingState", "UserFlags", "VerificationLevel", "VideoQualityMode", @@ -493,6 +495,8 @@ class MessageFlags(DiscordIntFlag): # type: ignore """This message should not trigger push or desktop notifications""" VOICE_MESSAGE = 1 << 13 """This message is a voice message""" + IS_COMPONENTS_V2 = 1 << 15 + """This message contains uses v2 components""" SUPPRESS_NOTIFICATIONS = SILENT """Alias for :attr:`SILENT`""" @@ -683,6 +687,54 @@ class ComponentType(CursedIntEnum): """Select menu for picking from mentionable objects""" CHANNEL_SELECT = 8 """Select menu for picking from channels""" + SECTION = 9 + """Section component for grouping together text and thumbnails/buttons""" + TEXT_DISPLAY = 10 + """Text component for displaying text""" + THUMBNAIL = 11 + """Thumbnail component for displaying a thumbnail for an image""" + MEDIA_GALLERY = 12 + """Media gallery component for displaying multiple images""" + FILE = 13 + """File component for uploading files""" + SEPARATOR = 14 + """Separator component for visual separation""" + CONTAINER = 17 + """Container component for grouping together other components""" + + # TODO: this is hacky, is there a better way to do this? + @staticmethod + def v2_component_types() -> set["ComponentType"]: + return { + ComponentType.SECTION, + ComponentType.TEXT_DISPLAY, + ComponentType.THUMBNAIL, + ComponentType.MEDIA_GALLERY, + ComponentType.FILE, + ComponentType.SEPARATOR, + ComponentType.CONTAINER, + } + + @property + def v2_component(self) -> bool: + """Whether this component is a v2 component.""" + return self.value in self.v2_component_types() + + +class UnfurledMediaItemLoadingState(CursedIntEnum): + """The loading state of an unfurled media item.""" + + UNKNOWN = 0 + LOADING = 1 + SUCCESS = 2 + FAILED = 3 + + +class SeparatorSpacingSize(CursedIntEnum): + """The size of the spacing in a separator component.""" + + SMALL = 1 + LARGE = 2 class IntegrationType(CursedIntEnum): diff --git a/interactions/models/discord/message.py b/interactions/models/discord/message.py index abe982653..9dbd34ff4 100644 --- a/interactions/models/discord/message.py +++ b/interactions/models/discord/message.py @@ -42,6 +42,7 @@ MessageFlags, MessageType, IntegrationType, + ComponentType, ) from .snowflake import ( Snowflake, @@ -1066,6 +1067,16 @@ def process_message_payload( embeds = embeds if all(e is not None for e in embeds) else None components = models.process_components(components) + if components: + # TODO: should we check for content/embeds? should this be moved elsewhere? + if any(c["type"] in ComponentType.v2_component_types() for c in components): + if not flags: + flags = 0 + flags |= MessageFlags.IS_COMPONENTS_V2 + + if content or embeds: + raise ValueError("Cannot send content or embeds with v2 components") + if stickers: stickers = [to_snowflake(sticker) for sticker in stickers] allowed_mentions = process_allowed_mentions(allowed_mentions) diff --git a/main.py b/main.py index 3946b1b0e..550425023 100644 --- a/main.py +++ b/main.py @@ -3,6 +3,8 @@ import os import uuid +from interactions.models.internal.context import SlashContext + from thefuzz import process import interactions @@ -69,6 +71,48 @@ async def components(ctx): ) +@slash_command("v2") +async def v2(ctx: SlashContext): + from interactions.models.discord.components import ( + SectionComponent, + Button, + TextDisplayComponent, + ContainerComponent, + ButtonStyle, + ActionRow, + SeparatorComponent, + ThumbnailComponent, + UnfurledMediaItem, + ) + + components = [ + SectionComponent( + components=[ + TextDisplayComponent("This is some"), + TextDisplayComponent("Text"), + ], + accessory=Button(style=ButtonStyle.PRIMARY, label="Click me"), + ), + TextDisplayComponent("Hello World"), + ContainerComponent( + ActionRow( + Button(style=ButtonStyle.RED, label="Red Button"), Button(style=ButtonStyle.GREEN, label="Green Button") + ), + SeparatorComponent(divider=True), + TextDisplayComponent("👀"), + ), + SectionComponent( + components=[ + TextDisplayComponent("This one has a thumbnail"), + ], + accessory=ThumbnailComponent( + UnfurledMediaItem("https://avatars.githubusercontent.com/u/98242689?s=200&v=4") + ), + ), + ] + await ctx.send(components=components) + + @slash_command("record", description="Record audio in your voice channel") @slash_option("duration", "The duration of the recording", opt_type=interactions.OptionType.NUMBER, required=True) async def record(ctx: interactions.SlashContext, duration: int) -> None: