Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: implement role subscription fields #904

Merged
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 3 additions & 0 deletions changelog/904.feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Add features related to role subscriptions.
- :attr:`MessageType.role_subscription_purchase`
- :attr:`RoleTags.subscription_listing_id`, :attr:`RoleTags.is_available_for_purchase`, and :attr:`RoleTags.is_subscription`
7 changes: 7 additions & 0 deletions disnake/emoji.py
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,9 @@ def roles(self) -> List[Role]:
"""List[:class:`Role`]: A :class:`list` of roles that are allowed to use this emoji.

If roles is empty, the emoji is unrestricted.

Emojis with :attr:`subscription roles <RoleTags.integration_id>` are considered premium emojis,
and count towards a separate limit of 25 emojis.
"""
guild = self.guild
if guild is None:
Expand Down Expand Up @@ -214,6 +217,10 @@ async def edit(
The new emoji name.
roles: Optional[List[:class:`~disnake.abc.Snowflake`]]
A list of roles that can use this emoji. An empty list can be passed to make it available to everyone.

An emoji cannot have both subscription roles (see :attr:`RoleTags.integration_id`) and
non-subscription roles, and emojis can't be converted between premium and non-premium
after creation.
reason: Optional[:class:`str`]
The reason for editing this emoji. Shows up on the audit log.

Expand Down
1 change: 1 addition & 0 deletions disnake/enums.py
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,7 @@ class MessageType(Enum):
guild_invite_reminder = 22
context_menu_command = 23
auto_moderation_action = 24
role_subscription_purchase = 25
interaction_premium_upsell = 26
guild_application_premium_subscription = 32

Expand Down
21 changes: 18 additions & 3 deletions disnake/guild.py
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,8 @@ class Guild(Hashable):
- ``AUTO_MODERATION``: Guild has set up auto moderation rules.
- ``BANNER``: Guild can upload and use a banner. (i.e. :attr:`.banner`)
- ``COMMUNITY``: Guild is a community server.
- ``CREATOR_MONETIZABLE_PROVISIONAL``: Guild has enabled monetization.
- ``CREATOR_STORE_PAGE``: Guild has enabled the role subscription promo page.
- ``DEVELOPER_SUPPORT_SERVER``: Guild is set as a support server in the app directory.
- ``DISCOVERABLE``: Guild shows up in Server Discovery.
- ``ENABLED_DISCOVERABLE_BEFORE``: Guild had Server Discovery enabled at least once.
Expand All @@ -217,7 +219,6 @@ class Guild(Hashable):
- ``INVITES_DISABLED``: Guild has paused invites, preventing new users from joining.
- ``LINKED_TO_HUB``: Guild is linked to a student hub.
- ``MEMBER_VERIFICATION_GATE_ENABLED``: Guild has Membership Screening enabled.
- ``MONETIZATION_ENABLED``: Guild has enabled monetization.
- ``MORE_EMOJI``: Guild has increased custom emoji slots.
- ``MORE_STICKERS``: Guild has increased custom sticker slots.
- ``NEWS``: Guild can create news channels.
Expand All @@ -226,6 +227,8 @@ class Guild(Hashable):
- ``PREVIEW_ENABLED``: Guild can be viewed before being accepted via Membership Screening.
- ``PRIVATE_THREADS``: Guild has access to create private threads (no longer has any effect).
- ``ROLE_ICONS``: Guild has access to role icons.
- ``ROLE_SUBSCRIPTIONS_AVAILABLE_FOR_PURCHASE``: Guild has role subscriptions that can be purchased.
- ``ROLE_SUBSCRIPTIONS_ENABLED``: Guild has enabled role subscriptions.
- ``SEVEN_DAY_THREAD_ARCHIVE``: Guild has access to the seven day archive time for threads (no longer has any effect).
- ``TEXT_IN_VOICE_ENABLED``: Guild has text in voice channels enabled (no longer has any effect).
- ``THREE_DAY_THREAD_ARCHIVE``: Guild has access to the three day archive time for threads (no longer has any effect).
Expand Down Expand Up @@ -847,7 +850,11 @@ def public_updates_channel(self) -> Optional[TextChannel]:

@property
def emoji_limit(self) -> int:
""":class:`int`: The maximum number of emoji slots this guild has."""
""":class:`int`: The maximum number of emoji slots this guild has.

Premium emojis (i.e. those associated with subscription roles) count towards a
separate limit of 25.
onerandomusername marked this conversation as resolved.
Show resolved Hide resolved
"""
more_emoji = 200 if "MORE_EMOJI" in self.features else 50
return max(more_emoji, self._PREMIUM_GUILD_LIMITS[self.premium_tier].emoji)

Expand Down Expand Up @@ -3240,6 +3247,9 @@ async def create_custom_emoji(
"2", "150"
"3", "250"

Emojis with subscription roles (see ``roles`` below) are considered premium emoji,
and count towards a separate limit of 25 emojis.

You must have :attr:`~Permissions.manage_emojis` permission to
do this.

Expand All @@ -3255,7 +3265,12 @@ async def create_custom_emoji(
Now accepts various resource types in addition to :class:`bytes`.

roles: List[:class:`Role`]
A :class:`list` of :class:`Role`\\s that can use this emoji. Leave empty to make it available to everyone.
A list of roles that can use this emoji. Leave empty to make it available to everyone.

An emoji cannot have both subscription roles (see :attr:`RoleTags.integration_id`) and
non-subscription roles, and emojis can't be converted between premium and non-premium
after creation.

reason: Optional[:class:`str`]
The reason for creating this emoji. Shows up on the audit log.

Expand Down
4 changes: 2 additions & 2 deletions disnake/integrations.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ class PartialIntegration:
guild: :class:`Guild`
The guild of the integration.
type: :class:`str`
The integration type (i.e. Twitch).
The integration type (i.e. ``twitch``).
account: :class:`IntegrationAccount`
The account linked to this integration.
application_id: Optional[:class:`int`]
Expand Down Expand Up @@ -119,7 +119,7 @@ class Integration(PartialIntegration):
Whether the integration is currently enabled.
account: :class:`IntegrationAccount`
The account linked to this integration.
user: :class:`User`
user: Optional[:class:`User`]
shiftinv marked this conversation as resolved.
Show resolved Hide resolved
The user that added this integration.
"""

Expand Down
3 changes: 3 additions & 0 deletions disnake/message.py
Original file line number Diff line number Diff line change
Expand Up @@ -1398,6 +1398,9 @@ def system_content(self) -> Optional[str]:
if self.type is MessageType.auto_moderation_action:
return self.content

# TODO: `MessageType.role_subscription_purchase` requires `Message.role_subscription_data`,
# which is currently undocumented

if self.type is MessageType.interaction_premium_upsell:
return self.content

Expand Down
63 changes: 57 additions & 6 deletions disnake/role.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,24 +46,37 @@ class RoleTags:
The bot's user ID that manages this role.
integration_id: Optional[:class:`int`]
The integration ID that manages the role.

Roles with this ID matching the guild's ``guild_subscription`` integration
onerandomusername marked this conversation as resolved.
Show resolved Hide resolved
are considered subscription roles.
subscription_listing_id: Optional[:class:`int`]
The ID of this role's subscription listing, if applicable.

.. versionadded:: 2.8
"""

__slots__ = (
"bot_id",
"integration_id",
"subscription_listing_id",
"_premium_subscriber",
"_guild_connections",
"_available_for_purchase",
)

def __init__(self, data: RoleTagPayload) -> None:
self.bot_id: Optional[int] = _get_as_snowflake(data, "bot_id")
self.integration_id: Optional[int] = _get_as_snowflake(data, "integration_id")
# NOTE: The API returns "null" for this if it's valid, which corresponds to None.
self.subscription_listing_id: Optional[int] = _get_as_snowflake(
data, "subscription_listing_id"
)

# NOTE: A value of null/None for this corresponds to True.
# If a field is missing, it corresponds to False.
# 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._guild_connections: Optional[Any] = data.get("guild_connections", MISSING)
self._available_for_purchase: Optional[Any] = data.get("available_for_purchase", MISSING)

def is_bot_managed(self) -> bool:
"""Whether the role is associated with a bot.
Expand All @@ -72,6 +85,13 @@ def is_bot_managed(self) -> bool:
"""
return self.bot_id is not None

def is_integration(self) -> bool:
"""Whether the role is managed by an integration.

:return type: :class:`bool`
"""
return self.integration_id is not None

def is_premium_subscriber(self) -> bool:
"""Whether the role is the premium subscriber, AKA "boost", role for the guild.

Expand All @@ -88,17 +108,30 @@ def is_linked_role(self) -> bool:
"""
return self._guild_connections is None

def is_integration(self) -> bool:
"""Whether the role is managed by an integration.
def is_available_for_purchase(self) -> bool:
"""Whether the role is a subscription role and available for purchase.

.. versionadded:: 2.8

:return type: :class:`bool`
"""
return self.integration_id is not None
return self._available_for_purchase is None

def is_subscription(self) -> bool:
"""Whether the role is associated with a role subscription.

.. versionadded:: 2.8

:return type: :class:`bool`
"""
return self.subscription_listing_id is not None

def __repr__(self) -> str:
return (
f"<RoleTags bot_id={self.bot_id} integration_id={self.integration_id} "
f"subscription_listing_id={self.subscription_listing_id} "
f"premium_subscriber={self.is_premium_subscriber()} "
f"available_for_purchase={self.is_available_for_purchase()} "
f"linked_role={self.is_linked_role()}>"
)

Expand Down Expand Up @@ -294,6 +327,24 @@ def is_integration(self) -> bool:
"""
return self.tags is not None and self.tags.is_integration()

def is_available_for_purchase(self) -> bool:
"""Whether the role is a subscription role and available for purchase.

.. versionadded:: 2.8

:return type: :class:`bool`
"""
return self.tags is not None and self.tags.is_available_for_purchase()

def is_subscription(self) -> bool:
"""Whether the role is associated with a role subscription.

.. versionadded:: 2.8

:return type: :class:`bool`
"""
return self.tags is not None and self.tags.is_subscription()

def is_assignable(self) -> bool:
"""Whether the role is able to be assigned or removed by the bot.

Expand Down
7 changes: 4 additions & 3 deletions disnake/types/guild.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ class UnavailableGuild(TypedDict):
"BANNER",
"COMMUNITY",
"CREATOR_MONETIZABLE", # not yet documented/finalised
"CREATOR_MONETIZABLE_PROVISIONAL",
"CREATOR_STORE_PAGE",
"DEVELOPER_SUPPORT_SERVER",
"DISCOVERABLE",
"ENABLED_DISCOVERABLE_BEFORE",
Expand All @@ -53,7 +55,6 @@ class UnavailableGuild(TypedDict):
"LINKED_TO_HUB",
"MEMBER_PROFILES", # not sure what this does, if anything
"MEMBER_VERIFICATION_GATE_ENABLED",
"MONETIZATION_ENABLED",
"MORE_EMOJI",
"MORE_STICKERS",
"NEWS",
Expand All @@ -63,8 +64,8 @@ class UnavailableGuild(TypedDict):
"PRIVATE_THREADS", # deprecated
"RELAY_ENABLED",
"ROLE_ICONS",
"ROLE_SUBSCRIPTIONS_AVAILABLE_FOR_PURCHASE", # not yet documented/finalised
"ROLE_SUBSCRIPTIONS_ENABLED", # not yet documented/finalised
"ROLE_SUBSCRIPTIONS_AVAILABLE_FOR_PURCHASE",
"ROLE_SUBSCRIPTIONS_ENABLED",
"SEVEN_DAY_THREAD_ARCHIVE", # deprecated
"TEXT_IN_VOICE_ENABLED", # deprecated
"THREADS_ENABLED", # deprecated
Expand Down
2 changes: 1 addition & 1 deletion disnake/types/integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ class PartialIntegration(TypedDict):
application_id: NotRequired[Snowflake]


IntegrationType = Literal["twitch", "youtube", "discord"]
IntegrationType = Literal["twitch", "youtube", "discord", "guild_subscription"]


class BaseIntegration(PartialIntegration):
Expand Down
2 changes: 1 addition & 1 deletion disnake/types/message.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ class MessageReference(TypedDict, total=False):


# fmt: off
MessageType = Literal[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 14, 15, 18, 19, 20, 21, 22, 23, 24, 26, 27, 28, 29, 30, 31, 32]
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, 26, 27, 28, 29, 30, 31, 32]
# fmt: on


Expand Down
2 changes: 2 additions & 0 deletions disnake/types/role.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ class RoleTags(TypedDict, total=False):
integration_id: Snowflake
premium_subscriber: None
guild_connections: None
subscription_listing_id: Snowflake
available_for_purchase: None


class CreateRole(TypedDict, total=False):
Expand Down
5 changes: 5 additions & 0 deletions docs/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1764,6 +1764,11 @@ of :class:`enum.Enum`.
The system message denoting that an auto moderation action was executed.

.. versionadded:: 2.5
.. attribute:: role_subscription_purchase

The system message denoting that a role subscription was purchased.

.. versionadded:: 2.8
shiftinv marked this conversation as resolved.
Show resolved Hide resolved
.. attribute:: interaction_premium_upsell

The system message for an application premium subscription upsell.
Expand Down