From 2dd8f923091bf1b7687bf777c5da62a1d430f862 Mon Sep 17 00:00:00 2001 From: Rapptz Date: Tue, 10 Jan 2023 18:05:00 -0500 Subject: [PATCH] Add support for role subscription reading --- discord/enums.py | 1 + discord/flags.py | 18 ++++++++++++ discord/message.py | 56 ++++++++++++++++++++++++++++++++++++ discord/role.py | 16 +++++++++++ discord/types/integration.py | 2 +- discord/types/message.py | 10 ++++++- discord/types/role.py | 2 ++ docs/api.rst | 16 +++++++++-- 8 files changed, 116 insertions(+), 5 deletions(-) diff --git a/discord/enums.py b/discord/enums.py index 49d82c7cd..a24adb7e3 100644 --- a/discord/enums.py +++ b/discord/enums.py @@ -284,6 +284,7 @@ class MessageType(Enum): guild_invite_reminder = 22 context_menu_command = 23 auto_moderation_action = 24 + role_subscription_purchase = 25 class SpeakingState(Enum): diff --git a/discord/flags.py b/discord/flags.py index da73a590d..7baca22c3 100644 --- a/discord/flags.py +++ b/discord/flags.py @@ -438,6 +438,24 @@ class SystemChannelFlags(BaseFlags): """ return 8 + @flag_value + def role_subscription_purchase_notifications(self): + """:class:`bool`: Returns ``True`` if role subscription purchase and renewal + notifications are enabled. + + .. versionadded:: 2.0 + """ + return 16 + + @flag_value + def role_subscription_purchase_notification_replies(self): + """:class:`bool`: Returns ``True`` if the role subscription notifications + have a sticker reply button. + + .. versionadded:: 2.0 + """ + return 32 + @fill_with_flags() class MessageFlags(BaseFlags): diff --git a/discord/message.py b/discord/message.py index 1308cf84e..8d812bb4b 100644 --- a/discord/message.py +++ b/discord/message.py @@ -79,6 +79,7 @@ if TYPE_CHECKING: MessageReference as MessageReferencePayload, BaseApplication as MessageApplicationPayload, MessageActivity as MessageActivityPayload, + RoleSubscriptionData as RoleSubscriptionDataPayload, ) from .types.interactions import MessageInteraction as MessageInteractionPayload @@ -111,6 +112,7 @@ __all__ = ( 'PartialMessage', 'MessageReference', 'DeletedReferencedMessage', + 'RoleSubscriptionInfo', ) @@ -554,6 +556,39 @@ def flatten_handlers(cls: Type[Message]) -> Type[Message]: return cls +class RoleSubscriptionInfo: + """Represents a message's role subscription information. + + This is currently only attached to messages of type :attr:`MessageType.role_subscription_purchase`. + + .. versionadded:: 2.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'] + + class PartialMessage(Hashable): """Represents a partial message to aid with working messages when only a message and channel ID are present. @@ -1257,6 +1292,11 @@ class Message(PartialMessage, Hashable): components: List[Union[:class:`ActionRow`, :class:`Button`, :class:`SelectMenu`]] A list of components in the message. + .. versionadded:: 2.0 + role_subscription: Optional[:class:`RoleSubscriptionInfo`] + The data of the role subscription purchase or renewal that prompted this + :attr:`MessageType.role_subscription_purchase` message. + .. versionadded:: 2.0 guild: Optional[:class:`Guild`] The guild that the message belongs to, if applicable. @@ -1300,6 +1340,7 @@ class Message(PartialMessage, Hashable): 'components', 'call', 'interaction', + 'role_subscription', ) if TYPE_CHECKING: @@ -1385,6 +1426,14 @@ class Message(PartialMessage, Hashable): # The channel will be the correct type here ref.resolved = self.__class__(channel=chan, data=resolved, state=state) # type: ignore + self.role_subscription: Optional[RoleSubscriptionInfo] = None + try: + role_subscription = data['role_subscription_data'] + except KeyError: + pass + else: + self.role_subscription = RoleSubscriptionInfo(role_subscription) + for handler in ('author', 'member', 'mentions', 'mention_roles', 'call', 'interaction', 'components'): try: getattr(self, f'_handle_{handler}')(data[handler]) @@ -1827,6 +1876,13 @@ class Message(PartialMessage, Hashable): 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: + # TODO: figure out how the message looks like for is_renewal: true + total_months = self.role_subscription.total_months_subscribed + months = '1 month' if total_months == 1 else f'{total_months} months' + return f'{self.author.name} joined {self.role_subscription.tier_name} and has been a subscriber of {self.guild} for {months}!' + + # Fallback for unknown message types return self.content @overload diff --git a/discord/role.py b/discord/role.py index fcd9f4e3c..aa2a53790 100644 --- a/discord/role.py +++ b/discord/role.py @@ -66,22 +66,31 @@ class RoleTags: The bot's user ID that manages this role. integration_id: Optional[:class:`int`] The integration ID that manages the role. + subscription_listing_id: Optional[:class:`int`] + The ID of this role's subscription SKU and listing. + + .. versionadded:: 2.0 """ __slots__ = ( 'bot_id', 'integration_id', '_premium_subscriber', + '_available_for_purchase', + 'subscription_listing_id', ) def __init__(self, data: RoleTagPayload): self.bot_id: Optional[int] = _get_as_snowflake(data, 'bot_id') self.integration_id: Optional[int] = _get_as_snowflake(data, 'integration_id') + self.subscription_listing_id: Optional[int] = _get_as_snowflake(data, 'subscription_listing_id') + # NOTE: The API returns "null" for this if it's valid, which corresponds to 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._available_for_purchase: Optional[Any] = data.get('available_for_purchase', MISSING) def is_bot_managed(self) -> bool: """:class:`bool`: Whether the role is associated with a bot.""" @@ -95,6 +104,13 @@ class RoleTags: """:class:`bool`: Whether the role is managed by an integration.""" return self.integration_id is not None + def is_available_for_purchase(self) -> bool: + """:class:`bool`: Whether the role is available for purchase. + + .. versionadded:: 2.0 + """ + return self._available_for_purchase is None + def __repr__(self) -> str: return ( f'