From 768befef05f254184bd12bb69c7daf2840eaeff4 Mon Sep 17 00:00:00 2001 From: AstreaTSS <25420078+AstreaTSS@users.noreply.github.com> Date: Wed, 21 Feb 2024 14:51:21 -0500 Subject: [PATCH 01/10] feat: add entitlement/app subscription support Untested as the only bot that I have that uses entitlements is in production, and can't be used for testing. --- interactions/__init__.py | 4 ++ interactions/api/events/__init__.py | 3 + interactions/api/events/discord.py | 29 ++++++++++ .../api/events/processors/__init__.py | 2 + .../events/processors/entitlement_events.py | 22 +++++++ interactions/api/http/http_client.py | 2 + .../api/http/http_requests/__init__.py | 2 + .../api/http/http_requests/entitlements.py | 57 +++++++++++++++++++ interactions/client/client.py | 42 ++++++++++++++ interactions/models/__init__.py | 4 ++ interactions/models/discord/__init__.py | 3 + interactions/models/discord/entitlement.py | 40 +++++++++++++ interactions/models/discord/enums.py | 6 ++ interactions/models/internal/context.py | 18 ++++++ 14 files changed, 234 insertions(+) create mode 100644 interactions/api/events/processors/entitlement_events.py create mode 100644 interactions/api/http/http_requests/entitlements.py create mode 100644 interactions/models/discord/entitlement.py diff --git a/interactions/__init__.py b/interactions/__init__.py index bb9b70b2a..f8b8b5a02 100644 --- a/interactions/__init__.py +++ b/interactions/__init__.py @@ -129,6 +129,7 @@ EmbedField, EmbedFooter, EmbedProvider, + Entitlement, ExplicitContentFilterLevel, Extension, File, @@ -229,6 +230,7 @@ ParagraphText, PartialEmoji, PartialEmojiConverter, + PartialEntitlement, PermissionOverwrite, Permissions, PremiumTier, @@ -455,6 +457,7 @@ "EmbedField", "EmbedFooter", "EmbedProvider", + "Entitlement", "errors", "events", "ExplicitContentFilterLevel", @@ -568,6 +571,7 @@ "ParagraphText", "PartialEmoji", "PartialEmojiConverter", + "PartialEntitlement", "PermissionOverwrite", "Permissions", "PREMIUM_GUILD_LIMITS", diff --git a/interactions/api/events/__init__.py b/interactions/api/events/__init__.py index f8d1789a2..25477e8f2 100644 --- a/interactions/api/events/__init__.py +++ b/interactions/api/events/__init__.py @@ -12,6 +12,9 @@ ChannelDelete, ChannelPinsUpdate, ChannelUpdate, + EntitlementCreate, + EntitlementDelete, + EntitlementUpdate, GuildAuditLogEntryCreate, GuildAvailable, GuildEmojisUpdate, diff --git a/interactions/api/events/discord.py b/interactions/api/events/discord.py index c621fd01f..182c5a467 100644 --- a/interactions/api/events/discord.py +++ b/interactions/api/events/discord.py @@ -43,6 +43,9 @@ async def an_event_handler(event: ChannelCreate): "ChannelDelete", "ChannelPinsUpdate", "ChannelUpdate", + "EntitlementCreate", + "EntitlementDelete", + "EntitlementUpdate", "GuildAuditLogEntryCreate", "GuildEmojisUpdate", "GuildJoin", @@ -108,6 +111,7 @@ async def an_event_handler(event: ChannelCreate): VoiceChannel, ) from interactions.models.discord.emoji import CustomEmoji, PartialEmoji + from interactions.models.discord.entitlement import Entitlement from interactions.models.discord.guild import Guild, GuildIntegration from interactions.models.discord.message import Message from interactions.models.discord.reaction import Reaction @@ -821,3 +825,28 @@ def member(self) -> Optional["Member"]: @attrs.define(eq=False, order=False, hash=False, kw_only=False) class GuildScheduledEventUserRemove(GuildScheduledEventUserAdd): """Dispatched when scheduled event is removed""" + + +@attrs.define(eq=False, order=False, hash=False, kw_only=False) +class BaseEntitlementEvent(BaseEvent): + entitlement: "Entitlement" = attrs.field(repr=True) + + +@attrs.define(eq=False, order=False, hash=False, kw_only=False) +class EntitlementCreate(BaseEntitlementEvent): + """Dispatched when a user subscribes to a SKU.""" + + +@attrs.define(eq=False, order=False, hash=False, kw_only=False) +class EntitlementUpdate(BaseEntitlementEvent): + """Dispatched when a user's subscription renews for the next billing period.""" + + +@attrs.define(eq=False, order=False, hash=False, kw_only=False) +class EntitlementDelete(BaseEntitlementEvent): + """ + Dispatched when a user's entitlement is deleted. + + Notably, this event is not dispatched when a user's subscription is cancelled. + Instead, you simply won't receive an EntitlementUpdate event for the next billing period. + """ diff --git a/interactions/api/events/processors/__init__.py b/interactions/api/events/processors/__init__.py index 750d145a4..65e0841d5 100644 --- a/interactions/api/events/processors/__init__.py +++ b/interactions/api/events/processors/__init__.py @@ -12,6 +12,7 @@ from .voice_events import VoiceEvents from ._template import Processor from .auto_mod import AutoModEvents +from .entitlement_events import EntitlementEvents __all__ = ( "ChannelEvents", @@ -28,4 +29,5 @@ "VoiceEvents", "Processor", "AutoModEvents", + "EntitlementEvents", ) diff --git a/interactions/api/events/processors/entitlement_events.py b/interactions/api/events/processors/entitlement_events.py new file mode 100644 index 000000000..ed806b530 --- /dev/null +++ b/interactions/api/events/processors/entitlement_events.py @@ -0,0 +1,22 @@ +from typing import TYPE_CHECKING + +from interactions.models.discord.entitlement import Entitlement +import interactions.api.events as events +from ._template import EventMixinTemplate, Processor + +if TYPE_CHECKING: + from interactions.api.events import RawGatewayEvent + + +class EntitlementEvents(EventMixinTemplate): + @Processor.define() + async def _on_raw_entitlement_create(self, event: "RawGatewayEvent") -> None: + self.dispatch(events.EntitlementCreate(Entitlement.from_dict(event.data, self))) + + @Processor.define() + async def _on_raw_entitlement_update(self, event: "RawGatewayEvent") -> None: + self.dispatch(events.EntitlementUpdate(Entitlement.from_dict(event.data, self))) + + @Processor.define() + async def _on_raw_entitlement_delete(self, event: "RawGatewayEvent") -> None: + self.dispatch(events.EntitlementDelete(Entitlement.from_dict(event.data, self))) diff --git a/interactions/api/http/http_client.py b/interactions/api/http/http_client.py index ac8578348..fd98caa86 100644 --- a/interactions/api/http/http_client.py +++ b/interactions/api/http/http_client.py @@ -19,6 +19,7 @@ BotRequests, ChannelRequests, EmojiRequests, + EntitlementRequests, GuildRequests, InteractionRequests, MemberRequests, @@ -204,6 +205,7 @@ class HTTPClient( BotRequests, ChannelRequests, EmojiRequests, + EntitlementRequests, GuildRequests, InteractionRequests, MemberRequests, diff --git a/interactions/api/http/http_requests/__init__.py b/interactions/api/http/http_requests/__init__.py index 8d6f72840..0e642aa02 100644 --- a/interactions/api/http/http_requests/__init__.py +++ b/interactions/api/http/http_requests/__init__.py @@ -1,6 +1,7 @@ from .bot import BotRequests from .channels import ChannelRequests from .emojis import EmojiRequests +from .entitlements import EntitlementRequests from .guild import GuildRequests from .interactions import InteractionRequests from .members import MemberRequests @@ -16,6 +17,7 @@ "BotRequests", "ChannelRequests", "EmojiRequests", + "EntitlementRequests", "GuildRequests", "InteractionRequests", "MemberRequests", diff --git a/interactions/api/http/http_requests/entitlements.py b/interactions/api/http/http_requests/entitlements.py new file mode 100644 index 000000000..5afae6dbd --- /dev/null +++ b/interactions/api/http/http_requests/entitlements.py @@ -0,0 +1,57 @@ +from typing import TYPE_CHECKING + +from ..route import Route +from interactions.models.internal.protocols import CanRequest + +if TYPE_CHECKING: + from interactions.models.discord.snowflake import Snowflake_Type + +__all__ = ("EntitlementRequests",) + + +class EntitlementRequests(CanRequest): + async def get_entitlements(self, application_id: "Snowflake_Type") -> list[dict]: + """ + Get an application's entitlements. + + Args: + application_id: The ID of the application. + + Returns: + A dictionary containing the application's entitlements. + """ + return await self.request( + Route("GET", "/applications/{application_id}/entitlements", application_id=application_id) + ) + + async def create_test_entitlement(self, payload: dict, application_id: "Snowflake_Type") -> dict: + """ + Create a test entitlement for an application. + + Args: + payload: The entitlement's data. + application_id: The ID of the application. + + Returns: + A dictionary containing the test entitlement. + """ + return await self.request( + Route("POST", "/applications/{application_id}/entitlements", application_id=application_id), payload=payload + ) + + async def delete_test_entitlement(self, application_id: "Snowflake_Type", entitlement_id: "Snowflake_Type") -> None: + """ + Delete a test entitlement for an application. + + Args: + application_id: The ID of the application. + entitlement_id: The ID of the entitlement. + """ + await self.request( + Route( + "DELETE", + "/applications/{application_id}/entitlements/{entitlement_id}", + application_id=application_id, + entitlement_id=entitlement_id, + ) + ) diff --git a/interactions/client/client.py b/interactions/client/client.py index 2d4da248f..7d3476107 100644 --- a/interactions/client/client.py +++ b/interactions/client/client.py @@ -87,6 +87,7 @@ from interactions.models.discord.color import BrandColors from interactions.models.discord.components import get_components_ids, BaseComponent from interactions.models.discord.embed import Embed +from interactions.models.discord.entitlement import Entitlement, PartialEntitlement from interactions.models.discord.enums import ( ComponentType, Intents, @@ -213,6 +214,7 @@ class Client( processors.AutoModEvents, processors.ChannelEvents, + processors.EntitlementEvents, processors.GuildEvents, processors.IntegrationEvents, processors.MemberEvents, @@ -2464,6 +2466,46 @@ def get_bot_voice_state(self, guild_id: "Snowflake_Type") -> Optional[ActiveVoic """ return self._connection_state.get_voice_state(guild_id) + async def fetch_entitlements(self) -> List[Entitlement]: + """ + Fetch the entitlements for the bot's application. + + Returns: + A list of entitlements. + """ + entitlements_data = await self.http.get_entitlements(self.app.id) + return Entitlement.from_list(entitlements_data, self) + + async def create_test_entitlement(self, sku_id: "Snowflake_Type", owner_id: "Snowflake_Type", owner_type: int) -> PartialEntitlement: + """ + Create a test entitlement for the bot's application. + + Args: + sku_id: The ID of the SKU to create the entitlement for. + owner_id: The ID of the owner of the entitlement. + owner_type: The type of the owner of the entitlement. 1 for a guild subscription, 2 for a user subscription + + Returns: + The created entitlement. + """ + payload = { + "sku_id": to_snowflake(sku_id), + "owner_id": to_snowflake(owner_id), + "owner_type": owner_type + } + + entitlement_data = await self.http.create_test_entitlement(payload, self.app.id) + return PartialEntitlement.from_dict(entitlement_data, self) + + async def delete_test_entitlement(self, entitlement_id: "Snowflake_Type") -> None: + """ + Delete a test entitlement for the bot's application. + + Args: + entitlement_id: The ID of the entitlement to delete. + """ + await self.http.delete_test_entitlement(to_snowflake(entitlement_id), self.app.id) + def mention_command(self, name: str, scope: int = 0) -> str: """ Returns a string that would mention the interaction specified. diff --git a/interactions/models/__init__.py b/interactions/models/__init__.py index 41ccfc7e0..d582725e6 100644 --- a/interactions/models/__init__.py +++ b/interactions/models/__init__.py @@ -54,6 +54,7 @@ EmbedField, EmbedFooter, EmbedProvider, + Entitlement, ExplicitContentFilterLevel, File, FlatUIColors, @@ -111,6 +112,7 @@ OverwriteType, ParagraphText, PartialEmoji, + PartialEntitlement, PermissionOverwrite, Permissions, PremiumTier, @@ -396,6 +398,7 @@ "EmbedField", "EmbedFooter", "EmbedProvider", + "Entitlement", "ExplicitContentFilterLevel", "Extension", "File", @@ -497,6 +500,7 @@ "ParagraphText", "PartialEmoji", "PartialEmojiConverter", + "PartialEntitlement", "PermissionOverwrite", "Permissions", "PremiumTier", diff --git a/interactions/models/discord/__init__.py b/interactions/models/discord/__init__.py index 035932e62..a4dcdca08 100644 --- a/interactions/models/discord/__init__.py +++ b/interactions/models/discord/__init__.py @@ -71,6 +71,7 @@ from .embed import Embed, EmbedAttachment, EmbedAuthor, EmbedField, EmbedFooter, EmbedProvider, process_embeds from .emoji import CustomEmoji, PartialEmoji, process_emoji, process_emoji_req_format +from .entitlement import Entitlement, PartialEntitlement from .enums import ( ActivityFlag, ActivityType, @@ -223,6 +224,7 @@ "EmbedField", "EmbedFooter", "EmbedProvider", + "Entitlement", "ExplicitContentFilterLevel", "File", "FlatUIColors", @@ -281,6 +283,7 @@ "OverwriteType", "ParagraphText", "PartialEmoji", + "PartialEntitlement", "PermissionOverwrite", "Permissions", "PremiumTier", diff --git a/interactions/models/discord/entitlement.py b/interactions/models/discord/entitlement.py new file mode 100644 index 000000000..b3886f1fd --- /dev/null +++ b/interactions/models/discord/entitlement.py @@ -0,0 +1,40 @@ +from typing import Optional, TYPE_CHECKING + +import attrs + +from interactions.models.discord.timestamp import Timestamp +from interactions.models.discord.enums import EntitlementType +from interactions.models.discord.base import DiscordObject +from interactions.client.utils.attr_converters import timestamp_converter +from interactions.models.discord.snowflake import to_snowflake, to_optional_snowflake, Snowflake_Type + +if TYPE_CHECKING: + from interactions.models.discord.guild import Guild + from interactions.models.discord.user import User + +__all__ = ("PartialEntitlement", "Entitlement") + + +@attrs.define(eq=False, order=False, hash=False, kw_only=False) +class PartialEntitlement(DiscordObject): + sku_id: Snowflake_Type = attrs.field(repr=False, converter=to_snowflake) + application_id: Snowflake_Type = attrs.field(repr=False, converter=to_snowflake) + type: EntitlementType = attrs.field(repr=False, converter=EntitlementType) + deleted: bool = attrs.field(repr=False, converter=bool) + _user_id: Optional[Snowflake_Type] = attrs.field(repr=False, converter=to_optional_snowflake) + _guild_id: Optional[Snowflake_Type] = attrs.field(repr=False, converter=to_optional_snowflake) + + @property + def user(self) -> "User": + return self.client.cache.get_user(self._user_id) + + @property + def guild(self) -> "Guild": + return self.client.cache.get_guild(self._guild_id) + + +@attrs.define(eq=False, order=False, hash=False, kw_only=False) +class Entitlement(PartialEntitlement): + subscription_id: Snowflake_Type = attrs.field(repr=False, converter=to_snowflake) + starts_at: Timestamp = attrs.field(repr=False, converter=timestamp_converter) + ends_at: Timestamp = attrs.field(repr=False, converter=timestamp_converter) diff --git a/interactions/models/discord/enums.py b/interactions/models/discord/enums.py index a89337ac0..4d247f5ef 100644 --- a/interactions/models/discord/enums.py +++ b/interactions/models/discord/enums.py @@ -1065,3 +1065,9 @@ class ForumSortOrder(CursedIntEnum): @classmethod def converter(cls, value: Optional[int]) -> "ForumSortOrder": return None if value is None else cls(value) + + +class EntitlementType(CursedIntEnum): + """The type of entitlement.""" + + APPLICATION_SUBSCRIPTION = 8 diff --git a/interactions/models/internal/context.py b/interactions/models/internal/context.py index 489e20866..af5084a94 100644 --- a/interactions/models/internal/context.py +++ b/interactions/models/internal/context.py @@ -19,6 +19,7 @@ from interactions.client.errors import HTTPException, AlreadyDeferred, AlreadyResponded from interactions.client.mixins.send import SendMixin +from interactions.models.discord.entitlement import Entitlement from interactions.models.discord.enums import ( Permissions, MessageFlags, @@ -252,6 +253,9 @@ class BaseInteractionContext(BaseContext): ephemeral: bool """Whether the interaction response is ephemeral.""" + entitlements: list[Entitlement] + """The entitlements of the invoking user.""" + _context_type: int """The context type of the interaction.""" command_id: Snowflake @@ -282,6 +286,7 @@ def from_dict(cls, client: "interactions.Client", payload: dict) -> Self: instance.guild_locale = payload.get("guild_locale", instance.locale) instance._context_type = payload.get("type", 0) instance.resolved = Resolved.from_dict(client, payload["data"].get("resolved", {}), payload.get("guild_id")) + instance.entitlements = [Entitlement.from_dict(ent, client) for ent in payload["entitlements"]] instance.channel_id = Snowflake(payload["channel_id"]) if channel := payload.get("channel"): @@ -418,6 +423,19 @@ async def defer(self, *, ephemeral: bool = False) -> None: self.deferred = True self.ephemeral = ephemeral + async def send_premium_required(self) -> None: + """ + Send a premium required response. + + When used, the user will be prompted to subscribe to premium to use this feature. + Only available for applications with monetization enabled. + """ + if self.responded: + raise RuntimeError("Cannot send a premium required response after responding") + + await self.client.http.post_initial_response({"type": 10}, self.id, self.token) + self.responded = True + async def _send_http_request( self, message_payload: dict, files: typing.Iterable["UPLOADABLE_TYPE"] | None = None ) -> dict: From 11935bc66e4a69242e03bf64153a50d866d40107 Mon Sep 17 00:00:00 2001 From: AstreaTSS <25420078+AstreaTSS@users.noreply.github.com> Date: Wed, 21 Feb 2024 15:00:04 -0500 Subject: [PATCH 02/10] refactor: use from_list instead of list comprehension --- interactions/models/internal/context.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/interactions/models/internal/context.py b/interactions/models/internal/context.py index af5084a94..616be2ccb 100644 --- a/interactions/models/internal/context.py +++ b/interactions/models/internal/context.py @@ -286,7 +286,7 @@ def from_dict(cls, client: "interactions.Client", payload: dict) -> Self: instance.guild_locale = payload.get("guild_locale", instance.locale) instance._context_type = payload.get("type", 0) instance.resolved = Resolved.from_dict(client, payload["data"].get("resolved", {}), payload.get("guild_id")) - instance.entitlements = [Entitlement.from_dict(ent, client) for ent in payload["entitlements"]] + instance.entitlements = Entitlement.from_list(payload["entitlements"], client) instance.channel_id = Snowflake(payload["channel_id"]) if channel := payload.get("channel"): From 3a71a8e9fd76ef26394c3af88fa7a4ad54b06be9 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 21 Feb 2024 20:02:31 +0000 Subject: [PATCH 03/10] ci: correct from checks. --- interactions/client/client.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/interactions/client/client.py b/interactions/client/client.py index 7d3476107..aad43b21b 100644 --- a/interactions/client/client.py +++ b/interactions/client/client.py @@ -2476,7 +2476,9 @@ async def fetch_entitlements(self) -> List[Entitlement]: entitlements_data = await self.http.get_entitlements(self.app.id) return Entitlement.from_list(entitlements_data, self) - async def create_test_entitlement(self, sku_id: "Snowflake_Type", owner_id: "Snowflake_Type", owner_type: int) -> PartialEntitlement: + async def create_test_entitlement( + self, sku_id: "Snowflake_Type", owner_id: "Snowflake_Type", owner_type: int + ) -> PartialEntitlement: """ Create a test entitlement for the bot's application. @@ -2488,11 +2490,7 @@ async def create_test_entitlement(self, sku_id: "Snowflake_Type", owner_id: "Sno Returns: The created entitlement. """ - payload = { - "sku_id": to_snowflake(sku_id), - "owner_id": to_snowflake(owner_id), - "owner_type": owner_type - } + payload = {"sku_id": to_snowflake(sku_id), "owner_id": to_snowflake(owner_id), "owner_type": owner_type} entitlement_data = await self.http.create_test_entitlement(payload, self.app.id) return PartialEntitlement.from_dict(entitlement_data, self) From 1725a10759c6b4e0ea9528fa3ee75b2d90c71773 Mon Sep 17 00:00:00 2001 From: AstreaTSS <25420078+AstreaTSS@users.noreply.github.com> Date: Wed, 21 Feb 2024 15:04:13 -0500 Subject: [PATCH 04/10] style: format client.py --- interactions/client/client.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/interactions/client/client.py b/interactions/client/client.py index 7d3476107..aad43b21b 100644 --- a/interactions/client/client.py +++ b/interactions/client/client.py @@ -2476,7 +2476,9 @@ async def fetch_entitlements(self) -> List[Entitlement]: entitlements_data = await self.http.get_entitlements(self.app.id) return Entitlement.from_list(entitlements_data, self) - async def create_test_entitlement(self, sku_id: "Snowflake_Type", owner_id: "Snowflake_Type", owner_type: int) -> PartialEntitlement: + async def create_test_entitlement( + self, sku_id: "Snowflake_Type", owner_id: "Snowflake_Type", owner_type: int + ) -> PartialEntitlement: """ Create a test entitlement for the bot's application. @@ -2488,11 +2490,7 @@ async def create_test_entitlement(self, sku_id: "Snowflake_Type", owner_id: "Sno Returns: The created entitlement. """ - payload = { - "sku_id": to_snowflake(sku_id), - "owner_id": to_snowflake(owner_id), - "owner_type": owner_type - } + payload = {"sku_id": to_snowflake(sku_id), "owner_id": to_snowflake(owner_id), "owner_type": owner_type} entitlement_data = await self.http.create_test_entitlement(payload, self.app.id) return PartialEntitlement.from_dict(entitlement_data, self) From ed446c5b7f6a0cc50d1e6c98ad16c1fab0e46cec Mon Sep 17 00:00:00 2001 From: AstreaTSS <25420078+AstreaTSS@users.noreply.github.com> Date: Wed, 21 Feb 2024 15:06:01 -0500 Subject: [PATCH 05/10] fix: oops --- interactions/api/events/__init__.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/interactions/api/events/__init__.py b/interactions/api/events/__init__.py index 25477e8f2..cccf69857 100644 --- a/interactions/api/events/__init__.py +++ b/interactions/api/events/__init__.py @@ -123,6 +123,9 @@ "ComponentError", "Connect", "Disconnect", + "EntitlementCreate", + "EntitlementDelete", + "EntitlementUpdate", "Error", "ExtensionCommandParse", "ExtensionLoad", From 7741ac283c29f98cedc6e9a85603f00058cfe15b Mon Sep 17 00:00:00 2001 From: AstreaTSS <25420078+AstreaTSS@users.noreply.github.com> Date: Wed, 21 Feb 2024 15:28:30 -0500 Subject: [PATCH 06/10] feat!: remove PartialEntitlement Turns out, there's a number of cases where it's impossible to know which entitlement you're dealing with. --- interactions/__init__.py | 2 -- interactions/client/client.py | 6 +++--- interactions/models/__init__.py | 2 -- interactions/models/discord/__init__.py | 3 +-- interactions/models/discord/entitlement.py | 23 ++++++++++------------ 5 files changed, 14 insertions(+), 22 deletions(-) diff --git a/interactions/__init__.py b/interactions/__init__.py index f8b8b5a02..50cdda95c 100644 --- a/interactions/__init__.py +++ b/interactions/__init__.py @@ -230,7 +230,6 @@ ParagraphText, PartialEmoji, PartialEmojiConverter, - PartialEntitlement, PermissionOverwrite, Permissions, PremiumTier, @@ -571,7 +570,6 @@ "ParagraphText", "PartialEmoji", "PartialEmojiConverter", - "PartialEntitlement", "PermissionOverwrite", "Permissions", "PREMIUM_GUILD_LIMITS", diff --git a/interactions/client/client.py b/interactions/client/client.py index aad43b21b..c4275c17b 100644 --- a/interactions/client/client.py +++ b/interactions/client/client.py @@ -87,7 +87,7 @@ from interactions.models.discord.color import BrandColors from interactions.models.discord.components import get_components_ids, BaseComponent from interactions.models.discord.embed import Embed -from interactions.models.discord.entitlement import Entitlement, PartialEntitlement +from interactions.models.discord.entitlement import Entitlement from interactions.models.discord.enums import ( ComponentType, Intents, @@ -2478,7 +2478,7 @@ async def fetch_entitlements(self) -> List[Entitlement]: async def create_test_entitlement( self, sku_id: "Snowflake_Type", owner_id: "Snowflake_Type", owner_type: int - ) -> PartialEntitlement: + ) -> Entitlement: """ Create a test entitlement for the bot's application. @@ -2493,7 +2493,7 @@ async def create_test_entitlement( payload = {"sku_id": to_snowflake(sku_id), "owner_id": to_snowflake(owner_id), "owner_type": owner_type} entitlement_data = await self.http.create_test_entitlement(payload, self.app.id) - return PartialEntitlement.from_dict(entitlement_data, self) + return Entitlement.from_dict(entitlement_data, self) async def delete_test_entitlement(self, entitlement_id: "Snowflake_Type") -> None: """ diff --git a/interactions/models/__init__.py b/interactions/models/__init__.py index d582725e6..e66a5a5aa 100644 --- a/interactions/models/__init__.py +++ b/interactions/models/__init__.py @@ -112,7 +112,6 @@ OverwriteType, ParagraphText, PartialEmoji, - PartialEntitlement, PermissionOverwrite, Permissions, PremiumTier, @@ -500,7 +499,6 @@ "ParagraphText", "PartialEmoji", "PartialEmojiConverter", - "PartialEntitlement", "PermissionOverwrite", "Permissions", "PremiumTier", diff --git a/interactions/models/discord/__init__.py b/interactions/models/discord/__init__.py index a4dcdca08..2168dd0c9 100644 --- a/interactions/models/discord/__init__.py +++ b/interactions/models/discord/__init__.py @@ -71,7 +71,7 @@ from .embed import Embed, EmbedAttachment, EmbedAuthor, EmbedField, EmbedFooter, EmbedProvider, process_embeds from .emoji import CustomEmoji, PartialEmoji, process_emoji, process_emoji_req_format -from .entitlement import Entitlement, PartialEntitlement +from .entitlement import Entitlement from .enums import ( ActivityFlag, ActivityType, @@ -283,7 +283,6 @@ "OverwriteType", "ParagraphText", "PartialEmoji", - "PartialEntitlement", "PermissionOverwrite", "Permissions", "PremiumTier", diff --git a/interactions/models/discord/entitlement.py b/interactions/models/discord/entitlement.py index b3886f1fd..79c9f4780 100644 --- a/interactions/models/discord/entitlement.py +++ b/interactions/models/discord/entitlement.py @@ -5,6 +5,7 @@ from interactions.models.discord.timestamp import Timestamp from interactions.models.discord.enums import EntitlementType from interactions.models.discord.base import DiscordObject +from interactions.client.utils.attr_converters import optional as optional_c from interactions.client.utils.attr_converters import timestamp_converter from interactions.models.discord.snowflake import to_snowflake, to_optional_snowflake, Snowflake_Type @@ -12,29 +13,25 @@ from interactions.models.discord.guild import Guild from interactions.models.discord.user import User -__all__ = ("PartialEntitlement", "Entitlement") +__all__ = ("Entitlement",) @attrs.define(eq=False, order=False, hash=False, kw_only=False) -class PartialEntitlement(DiscordObject): +class Entitlement(DiscordObject): sku_id: Snowflake_Type = attrs.field(repr=False, converter=to_snowflake) application_id: Snowflake_Type = attrs.field(repr=False, converter=to_snowflake) type: EntitlementType = attrs.field(repr=False, converter=EntitlementType) deleted: bool = attrs.field(repr=False, converter=bool) - _user_id: Optional[Snowflake_Type] = attrs.field(repr=False, converter=to_optional_snowflake) - _guild_id: Optional[Snowflake_Type] = attrs.field(repr=False, converter=to_optional_snowflake) + subscription_id: Optional[Snowflake_Type] = attrs.field(repr=False, converter=to_optional_snowflake, default=None) + starts_at: Optional[Timestamp] = attrs.field(repr=False, converter=optional_c(timestamp_converter), default=None) + ends_at: Optional[Timestamp] = attrs.field(repr=False, converter=optional_c(timestamp_converter), default=None) + _user_id: Optional[Snowflake_Type] = attrs.field(repr=False, converter=to_optional_snowflake, default=None) + _guild_id: Optional[Snowflake_Type] = attrs.field(repr=False, converter=to_optional_snowflake, default=None) @property - def user(self) -> "User": + def user(self) -> "User | None": return self.client.cache.get_user(self._user_id) @property - def guild(self) -> "Guild": + def guild(self) -> "Guild | None": return self.client.cache.get_guild(self._guild_id) - - -@attrs.define(eq=False, order=False, hash=False, kw_only=False) -class Entitlement(PartialEntitlement): - subscription_id: Snowflake_Type = attrs.field(repr=False, converter=to_snowflake) - starts_at: Timestamp = attrs.field(repr=False, converter=timestamp_converter) - ends_at: Timestamp = attrs.field(repr=False, converter=timestamp_converter) From 788b27564057eab1a583024e8c958c59c75ed64f Mon Sep 17 00:00:00 2001 From: AstreaTSS <25420078+AstreaTSS@users.noreply.github.com> Date: Wed, 21 Feb 2024 15:33:20 -0500 Subject: [PATCH 07/10] fix: pass in arguments correctly to delete_test_entitlement --- interactions/client/client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/interactions/client/client.py b/interactions/client/client.py index c4275c17b..68c70c0c6 100644 --- a/interactions/client/client.py +++ b/interactions/client/client.py @@ -2502,7 +2502,7 @@ async def delete_test_entitlement(self, entitlement_id: "Snowflake_Type") -> Non Args: entitlement_id: The ID of the entitlement to delete. """ - await self.http.delete_test_entitlement(to_snowflake(entitlement_id), self.app.id) + await self.http.delete_test_entitlement(self.app.id, to_snowflake(entitlement_id)) def mention_command(self, name: str, scope: int = 0) -> str: """ From f8de55058c6eea777b4a220aef56e57c4d952f65 Mon Sep 17 00:00:00 2001 From: AstreaTSS <25420078+AstreaTSS@users.noreply.github.com> Date: Wed, 21 Feb 2024 15:51:36 -0500 Subject: [PATCH 08/10] docs: add docstrings to Entitlement --- interactions/models/discord/entitlement.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/interactions/models/discord/entitlement.py b/interactions/models/discord/entitlement.py index 79c9f4780..bb14ba52f 100644 --- a/interactions/models/discord/entitlement.py +++ b/interactions/models/discord/entitlement.py @@ -19,19 +19,30 @@ @attrs.define(eq=False, order=False, hash=False, kw_only=False) class Entitlement(DiscordObject): sku_id: Snowflake_Type = attrs.field(repr=False, converter=to_snowflake) + """ID of the SKU.""" application_id: Snowflake_Type = attrs.field(repr=False, converter=to_snowflake) + """ID of the parent application.""" type: EntitlementType = attrs.field(repr=False, converter=EntitlementType) + """The type of entitlement.""" deleted: bool = attrs.field(repr=False, converter=bool) + """Whether the entitlement is deleted.""" subscription_id: Optional[Snowflake_Type] = attrs.field(repr=False, converter=to_optional_snowflake, default=None) + """The ID of the subscription plan. Not present when using test entitlements.""" starts_at: Optional[Timestamp] = attrs.field(repr=False, converter=optional_c(timestamp_converter), default=None) + """Start date at which the entitlement is valid. Not present when using test entitlements.""" ends_at: Optional[Timestamp] = attrs.field(repr=False, converter=optional_c(timestamp_converter), default=None) + """Date at which the entitlement is no longer valid. Not present when using test entitlements.""" _user_id: Optional[Snowflake_Type] = attrs.field(repr=False, converter=to_optional_snowflake, default=None) + """The ID of the user that is granted access to the entitlement's SKU.""" _guild_id: Optional[Snowflake_Type] = attrs.field(repr=False, converter=to_optional_snowflake, default=None) + """The ID of the guild that is granted access to the entitlement's SKU.""" @property - def user(self) -> "User | None": + def user(self) -> "Optional[User]": + """The user that is granted access to the entitlement's SKU, if applicable.""" return self.client.cache.get_user(self._user_id) @property - def guild(self) -> "Guild | None": + def guild(self) -> "Optional[Guild]": + """The guild that is granted access to the entitlement's SKU, if applicable.""" return self.client.cache.get_guild(self._guild_id) From 359a23429de98d581c25b40574063571593a6d26 Mon Sep 17 00:00:00 2001 From: AstreaTSS <25420078+AstreaTSS@users.noreply.github.com> Date: Wed, 21 Feb 2024 16:02:28 -0500 Subject: [PATCH 09/10] feat: add in params for get_entitlements --- .../api/http/http_requests/entitlements.py | 39 +++++++++++++++++-- interactions/client/client.py | 32 ++++++++++++++- 2 files changed, 65 insertions(+), 6 deletions(-) diff --git a/interactions/api/http/http_requests/entitlements.py b/interactions/api/http/http_requests/entitlements.py index 5afae6dbd..8ab4e15aa 100644 --- a/interactions/api/http/http_requests/entitlements.py +++ b/interactions/api/http/http_requests/entitlements.py @@ -1,7 +1,9 @@ -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Optional -from ..route import Route +from ..route import Route, PAYLOAD_TYPE from interactions.models.internal.protocols import CanRequest +from interactions.models.discord.snowflake import to_optional_snowflake, to_snowflake +from interactions.client.utils.serializer import dict_filter_none if TYPE_CHECKING: from interactions.models.discord.snowflake import Snowflake_Type @@ -10,18 +12,47 @@ class EntitlementRequests(CanRequest): - async def get_entitlements(self, application_id: "Snowflake_Type") -> list[dict]: + async def get_entitlements( + self, + application_id: "Snowflake_Type", + *, + user_id: "Optional[Snowflake_Type]" = None, + sku_ids: "Optional[list[Snowflake_Type]]" = None, + before: "Optional[Snowflake_Type]" = None, + after: "Optional[Snowflake_Type]" = None, + limit: Optional[int] = 100, + guild_id: "Optional[Snowflake_Type]" = None, + exclude_ended: Optional[bool] = None, + ) -> list[dict]: """ Get an application's entitlements. Args: application_id: The ID of the application. + user_id: The ID of the user to filter entitlements by. + sku_ids: The IDs of the SKUs to filter entitlements by. + before: Get entitlements before this ID. + after: Get entitlements after this ID. + limit: The maximum number of entitlements to return. Maximum is 100. + guild_id: The ID of the guild to filter entitlements by. + exclude_ended: Whether to exclude ended entitlements. Returns: A dictionary containing the application's entitlements. """ + params: PAYLOAD_TYPE = { + "user_id": to_optional_snowflake(user_id), + "sku_ids": [to_snowflake(sku_id) for sku_id in sku_ids] if sku_ids else None, + "before": to_optional_snowflake(before), + "after": to_optional_snowflake(after), + "limit": limit, + "guild_id": to_optional_snowflake(guild_id), + "exclude_ended": exclude_ended, + } + params = dict_filter_none(params) + return await self.request( - Route("GET", "/applications/{application_id}/entitlements", application_id=application_id) + Route("GET", "/applications/{application_id}/entitlements", application_id=application_id), params=params ) async def create_test_entitlement(self, payload: dict, application_id: "Snowflake_Type") -> dict: diff --git a/interactions/client/client.py b/interactions/client/client.py index 68c70c0c6..729998bbd 100644 --- a/interactions/client/client.py +++ b/interactions/client/client.py @@ -2466,14 +2466,42 @@ def get_bot_voice_state(self, guild_id: "Snowflake_Type") -> Optional[ActiveVoic """ return self._connection_state.get_voice_state(guild_id) - async def fetch_entitlements(self) -> List[Entitlement]: + async def fetch_entitlements( + self, + *, + user_id: "Optional[Snowflake_Type]" = None, + sku_ids: "Optional[list[Snowflake_Type]]" = None, + before: "Optional[Snowflake_Type]" = None, + after: "Optional[Snowflake_Type]" = None, + limit: Optional[int] = 100, + guild_id: "Optional[Snowflake_Type]" = None, + exclude_ended: Optional[bool] = None, + ) -> List[Entitlement]: """ Fetch the entitlements for the bot's application. + Args: + user_id: The ID of the user to filter entitlements by. + sku_ids: The IDs of the SKUs to filter entitlements by. + before: Get entitlements before this ID. + after: Get entitlements after this ID. + limit: The maximum number of entitlements to return. Maximum is 100. + guild_id: The ID of the guild to filter entitlements by. + exclude_ended: Whether to exclude ended entitlements. + Returns: A list of entitlements. """ - entitlements_data = await self.http.get_entitlements(self.app.id) + entitlements_data = await self.http.get_entitlements( + self.app.id, + user_id=user_id, + sku_ids=sku_ids, + before=before, + after=after, + limit=limit, + guild_id=guild_id, + exclude_ended=exclude_ended, + ) return Entitlement.from_list(entitlements_data, self) async def create_test_entitlement( From de1e441a7b467f886fd2e85e2e01a07fc022d800 Mon Sep 17 00:00:00 2001 From: AstreaTSS <25420078+AstreaTSS@users.noreply.github.com> Date: Wed, 21 Feb 2024 18:52:19 -0500 Subject: [PATCH 10/10] docs: add entitlements to models index --- .../API Reference/API Reference/models/Discord/entitlement.md | 1 + docs/src/API Reference/API Reference/models/Discord/index.md | 1 + 2 files changed, 2 insertions(+) create mode 100644 docs/src/API Reference/API Reference/models/Discord/entitlement.md diff --git a/docs/src/API Reference/API Reference/models/Discord/entitlement.md b/docs/src/API Reference/API Reference/models/Discord/entitlement.md new file mode 100644 index 000000000..8211c542a --- /dev/null +++ b/docs/src/API Reference/API Reference/models/Discord/entitlement.md @@ -0,0 +1 @@ +::: interactions.models.discord.entitlement diff --git a/docs/src/API Reference/API Reference/models/Discord/index.md b/docs/src/API Reference/API Reference/models/Discord/index.md index d56908be7..1b45617cd 100644 --- a/docs/src/API Reference/API Reference/models/Discord/index.md +++ b/docs/src/API Reference/API Reference/models/Discord/index.md @@ -14,6 +14,7 @@ search: - [Components](components) - [Embed](embed) - [Emoji](emoji) +- [Entitlement](entitlement) - [Enums](enums) - [File](file) - [Guild](guild)