diff --git a/docs/api.rst b/docs/api.rst index 3edf3af46b..41370f2bfb 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -1516,6 +1516,12 @@ of :class:`enum.Enum`. .. versionadded:: 2.6 + .. attribute:: role_subscription_purchase + + The system message denoting that a role subscription was purchased. + + .. versionadded:: 3.0 + .. class:: UserFlags Represents Discord User flags. diff --git a/nextcord/enums.py b/nextcord/enums.py index 20855a33f1..b53c19c4e9 100644 --- a/nextcord/enums.py +++ b/nextcord/enums.py @@ -180,6 +180,7 @@ class MessageType(IntEnum): guild_invite_reminder = 22 context_menu_command = 23 auto_moderation_action = 24 + role_subscription_purchase = 25 stage_start = 27 stage_end = 28 stage_speaker = 29 diff --git a/nextcord/flags.py b/nextcord/flags.py index a3e1d2085a..66c460800f 100644 --- a/nextcord/flags.py +++ b/nextcord/flags.py @@ -222,6 +222,22 @@ def join_notification_replies(self) -> int: """ return 8 + @flag_value + def role_subscription_purchase_notifications(self) -> int: + """:class:`bool`: Returns ``True`` if the system channel is used for role subscription purchase notifications. + + .. versionadded:: 2.6 + """ + return 16 + + @flag_value + def role_subscription_purchase_notification_replies(self) -> int: + """:class:`bool`: Returns ``True`` if the button to reply with a sticker to role subscription purchase notifications is shown. + + .. versionadded:: 2.6 + """ + return 32 + @fill_with_flags() class ChannelFlags(BaseFlags): diff --git a/nextcord/guild.py b/nextcord/guild.py index f2cd68d3be..aeb0854b91 100644 --- a/nextcord/guild.py +++ b/nextcord/guild.py @@ -224,6 +224,8 @@ class Guild(Hashable): - ``VERIFIED``: Guild is a verified server. - ``VIP_REGIONS``: Guild has VIP voice regions. - ``WELCOME_SCREEN_ENABLED``: Guild has enabled the welcome screen. + - ``ROLE_SUBSCRIPTIONS_ENABLED``: Guild has enabled role subscriptions. + - ``ROLE_SUBSCRIPTIONS_AVAILABLE_FOR_PURCHASE``: Guild has role subscriptions that can be purchased. premium_tier: :class:`int` The premium tier for this guild. Corresponds to "Boost Level" in the official UI. diff --git a/nextcord/message.py b/nextcord/message.py index 93885712fe..a079866343 100644 --- a/nextcord/message.py +++ b/nextcord/message.py @@ -57,6 +57,7 @@ MessageApplication as MessageApplicationPayload, MessageReference as MessageReferencePayload, Reaction as ReactionPayload, + RoleSubscriptionData as RoleSubscriptionDataPayload, ) from .types.threads import Thread as ThreadPayload, ThreadArchiveDuration from .types.user import User as UserPayload @@ -72,6 +73,7 @@ "MessageReference", "DeletedReferencedMessage", "MessageInteraction", + "MessageRoleSubscription", ) @@ -619,6 +621,39 @@ def created_at(self) -> datetime.datetime: return utils.snowflake_time(self.id) +class MessageRoleSubscription: + """Represents a message's role subscription information. + + This is accessed through the :attr:`Message.role_subscription` attribute if the :attr:`Message.type` is :attr:`MessageType.role_subscription_purchase`. + + .. versionadded:: 3.0 + + Attributes + ---------- + role_subscription_listing_id: :class:`int` + The ID of the SKU and listing that the user is subscribed to. + tier_name: :class:`str` + The name of the tier that the user is subscribed to. + total_months_subscribed: :class:`int` + The cumulative number of months that the user has been subscribed for. + is_renewal: :class:`bool` + Whether this notification is for a renewal rather than a new purchase. + """ + + __slots__ = ( + "role_subscription_listing_id", + "tier_name", + "total_months_subscribed", + "is_renewal", + ) + + def __init__(self, data: RoleSubscriptionDataPayload) -> None: + self.role_subscription_listing_id: int = int(data["role_subscription_listing_id"]) + self.tier_name: str = data["tier_name"] + self.total_months_subscribed: int = data["total_months_subscribed"] + self.is_renewal: bool = data["is_renewal"] + + @flatten_handlers class Message(Hashable): r"""Represents a message from Discord. @@ -736,6 +771,10 @@ class Message(Hashable): The guild that the message belongs to, if applicable. interaction: Optional[:class:`MessageInteraction`] The interaction data of a message, if applicable. + role_subscription: Optional[:class:`MessageRoleSubscription`] + The role subscription data of a message, if applicable. + + .. versionadded:: 2.6 """ __slots__ = ( @@ -770,6 +809,7 @@ class Message(Hashable): "stickers", "components", "guild", + "role_subscription", ) if TYPE_CHECKING: @@ -865,6 +905,11 @@ def __init__( if "interaction" in data else None ) + self.role_subscription: Optional[MessageRoleSubscription] = ( + MessageRoleSubscription(data=data["role_subscription_data"]) + if "role_subscription_data" in data + else None + ) def __repr__(self) -> str: name = self.__class__.__name__ @@ -1282,6 +1327,18 @@ def system_content(self): if self.type is MessageType.guild_invite_reminder: return "Wondering who to invite?\nStart by inviting anyone who can help you build the server!" + if ( + self.type is MessageType.role_subscription_purchase + and self.role_subscription is not None + ): + tier_name = self.role_subscription.tier_name + total_months_subscribed = self.role_subscription.total_months_subscribed + months = f"{total_months_subscribed} month{'s' if total_months_subscribed != 1 else ''}" + if self.role_subscription.is_renewal: + return f"{self.author.name} renewed {tier_name} and has been a subscriber of {self.guild} for {months}!" + + return f"{self.author.name} joined {tier_name} and has been a subscriber of {self.guild} for {months}!" + if self.type is MessageType.stage_start: return f"{self.author.display_name} started {self.content}" diff --git a/nextcord/role.py b/nextcord/role.py index a8270b7b07..dff5b89754 100644 --- a/nextcord/role.py +++ b/nextcord/role.py @@ -51,16 +51,28 @@ class RoleTags: subscription_listing_id: Optional[:class:`int`] The ID of the subscription listing that manages the role. - .. versionadded:: 2.4 + .. versionadded:: 3.0 + premium_subscriber: :class:`bool` + Whether the role is the premium subscriber, AKA "boost", role for the guild. + + versionadded:: 3.0 + available_for_purchase: :class:`bool` + Whether the role is available for purchase. + + .. versionadded:: 3.0 + guild_connections: :class:`bool` + Whether the role is a guild's linked role. + + .. versionadded:: 3.0 """ __slots__ = ( "bot_id", "integration_id", - "_premium_subscriber", "subscription_listing_id", - "_available_for_purchase", - "_guild_connections", + "premium_subscriber", + "available_for_purchase", + "guild_connections", ) def __init__(self, data: RoleTagPayload) -> None: @@ -70,12 +82,12 @@ def __init__(self, data: RoleTagPayload) -> None: # This is different from other fields where "null" means "not there". # So in this case, a value of None is the same as True. # Which means we would need a different sentinel. - self._premium_subscriber: Optional[Any] = data.get("premium_subscriber", MISSING) self.subscription_listing_id: Optional[int] = get_as_snowflake( data, "subscription_listing_id" ) - self._available_for_purchase: Optional[Any] = data.get("available_for_purchase", MISSING) - self._guild_connections: Optional[Any] = data.get("guild_connections", MISSING) + self.guild_connections: bool = "guild_connections" in data + self.premium_subscriber: bool = "premium_subscriber" in data + self.available_for_purchase: bool = "available_for_purchase" in data def is_bot_managed(self) -> bool: """:class:`bool`: Whether the role is associated with a bot.""" @@ -83,7 +95,7 @@ def is_bot_managed(self) -> bool: def is_premium_subscriber(self) -> bool: """:class:`bool`: Whether the role is the premium subscriber, AKA "boost", role for the guild.""" - return self._premium_subscriber is None + return self.premium_subscriber def is_integration(self) -> bool: """:class:`bool`: Whether the role is managed by an integration.""" @@ -94,14 +106,14 @@ def is_available_for_purchase(self) -> bool: .. versionadded:: 2.4 """ - return self._available_for_purchase is None + return self.available_for_purchase def has_guild_connections(self) -> bool: """:class:`bool`: Whether the role is a guild's linked role. .. versionadded:: 2.4 """ - return self._guild_connections is None + return self.guild_connections def __repr__(self) -> str: return ( diff --git a/nextcord/types/guild.py b/nextcord/types/guild.py index a942b75bf8..ef1f21afc9 100644 --- a/nextcord/types/guild.py +++ b/nextcord/types/guild.py @@ -59,6 +59,8 @@ class UnavailableGuild(TypedDict): "VERIFIED", "VIP_REGIONS", "WELCOME_SCREEN_ENABLED", + "ROLE_SUBSCRIPTIONS_AVAILABLE_FOR_PURCHASE", + "ROLE_SUBSCRIPTIONS_ENABLED", ] diff --git a/nextcord/types/integration.py b/nextcord/types/integration.py index f3f7fe4a6c..2d1eac0a5f 100644 --- a/nextcord/types/integration.py +++ b/nextcord/types/integration.py @@ -34,7 +34,7 @@ class PartialIntegration(TypedDict): account: IntegrationAccount -IntegrationType = Literal["twitch", "youtube", "discord"] +IntegrationType = Literal["twitch", "youtube", "discord", "guild_subscription"] class BaseIntegration(PartialIntegration): diff --git a/nextcord/types/message.py b/nextcord/types/message.py index bb99e762c0..126caa00ba 100644 --- a/nextcord/types/message.py +++ b/nextcord/types/message.py @@ -67,7 +67,16 @@ class MessageReference(TypedDict, total=False): fail_if_not_exists: bool -MessageType = Literal[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 14, 15, 18, 19, 20, 21] +class RoleSubscriptionData(TypedDict): + role_subscription_listing_id: Snowflake + tier_name: str + total_months_subscribed: int + is_renewal: bool + + +MessageType = Literal[ + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 14, 15, 18, 19, 20, 21, 22, 23, 24, 25 +] class Message(TypedDict): @@ -100,6 +109,7 @@ class Message(TypedDict): referenced_message: NotRequired[Optional[Message]] interaction: NotRequired[MessageInteraction] components: NotRequired[List[Component]] + role_subscription_data: NotRequired[RoleSubscriptionData] AllowedMentionType = Literal["roles", "users", "everyone"]