diff --git a/CHANGELOG.md b/CHANGELOG.md index 35321c2858..ae6887be2e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,8 @@ These changes are available on the `master` branch, but have not yet been releas - Added `Attachment.read_chunked` and added optional `chunksize` argument to `Attachment.save` for retrieving attachments in chunks. ([#2956](https://github.com/Pycord-Development/pycord/pull/2956)) +- Added `AppInfo.edit()` method and missing fields. + ([#2994](https://github.com/Pycord-Development/pycord/pull/2994)) ### Changed diff --git a/discord/appinfo.py b/discord/appinfo.py index 034b1bb158..5b8fded5d0 100644 --- a/discord/appinfo.py +++ b/discord/appinfo.py @@ -29,6 +29,7 @@ from . import utils from .asset import Asset +from .flags import ApplicationFlags from .permissions import Permissions if TYPE_CHECKING: @@ -44,6 +45,7 @@ "AppInfo", "PartialAppInfo", "AppInstallParams", + "IntegrationTypesConfig", ) @@ -134,11 +136,32 @@ class AppInfo: .. versionadded:: 2.7 - install_params: Optional[List[:class:`AppInstallParams`]] + install_params: Optional[:class:`AppInstallParams`] The settings for the application's default in-app authorization link, if set. .. versionadded:: 2.7 + integration_types_config: Optional[:class:`IntegrationTypesConfig`] + Per-installation context configuration for guild (``0``) and user (``1``) contexts. + + .. versionadded:: 2.7 + + event_webhooks_url: Optional[:class:`str`] + The URL used to receive application event webhooks, if set. + + .. versionadded:: 2.7 + + event_webhooks_status: Optional[:class:`int`] + The raw event webhooks status integer from the API (``2`` enabled, ``1`` disabled) if present. + Prefer :attr:`event_webhooks_enabled` for a boolean form. + + .. versionadded:: 2.7 + + event_webhooks_types: Optional[List[:class:`str`]] + List of event webhook types subscribed to, if set. + + .. versionadded:: 2.7 + tags: Optional[List[:class:`str`]] The list of tags describing the content and functionality of the app, if set. @@ -149,6 +172,21 @@ class AppInfo: custom_install_url: Optional[:class:`str`] The default custom authorization URL for the application, if set. + .. versionadded:: 2.7 + + approximate_user_authorization_count: Optional[:class:`int`] + The approximate count of users who have authorized the application, if any. + + .. versionadded:: 2.7 + + flags: Optional[:class:`ApplicationFlags`] + The public application flags, if set. + + .. versionadded:: 2.7 + + bot: Optional[:class:`User`] + The bot user associated with this application, if any. + .. versionadded:: 2.7 """ @@ -161,6 +199,7 @@ class AppInfo: "bot_public", "bot_require_code_grant", "owner", + "bot", "_icon", "_summary", "verify_key", @@ -173,9 +212,15 @@ class AppInfo: "privacy_policy_url", "approximate_guild_count", "approximate_user_install_count", + "approximate_user_authorization_count", + "_flags", "redirect_uris", "interactions_endpoint_url", "role_connections_verification_url", + "event_webhooks_url", + "event_webhooks_status", + "event_webhooks_types", + "integration_types_config", "install_params", "tags", "custom_install_url", @@ -188,17 +233,18 @@ def __init__(self, state: ConnectionState, data: AppInfoPayload): self.id: int = int(data["id"]) self.name: str = data["name"] self.description: str = data["description"] - self._icon: str | None = data["icon"] - self.rpc_origins: list[str] = data["rpc_origins"] - self.bot_public: bool = data["bot_public"] - self.bot_require_code_grant: bool = data["bot_require_code_grant"] - self.owner: User = state.create_user(data["owner"]) + self._icon: str | None = data.get("icon") + self.rpc_origins: list[str] | None = data.get("rpc_origins") + self.bot_public: bool = data.get("bot_public", False) + self.bot_require_code_grant: bool = data.get("bot_require_code_grant", False) + self.owner: User | None = data.get("owner") and state.create_user(data["owner"]) team: TeamPayload | None = data.get("team") self.team: Team | None = Team(state, team) if team else None self._summary: str = data["summary"] self.verify_key: str = data["verify_key"] + self.bot: User | None = data.get("bot") and state.create_user(data["bot"]) self.guild_id: int | None = utils._get_as_snowflake(data, "guild_id") @@ -213,20 +259,29 @@ def __init__(self, state: ConnectionState, data: AppInfoPayload): self.approximate_user_install_count: int | None = data.get( "approximate_user_install_count" ) - self.redirect_uris: list[str] | None = data.get("redirect_uris", []) + self.approximate_user_authorization_count: int | None = data.get( + "approximate_user_authorization_count" + ) + self._flags: int | None = data.get("flags") + self.redirect_uris: list[str] = data.get("redirect_uris", []) self.interactions_endpoint_url: str | None = data.get( "interactions_endpoint_url" ) self.role_connections_verification_url: str | None = data.get( "role_connections_verification_url" ) + self.event_webhooks_url: str | None = data.get("event_webhooks_url") + self.event_webhooks_status: int | None = data.get("event_webhooks_status") + self.event_webhooks_types: list[str] | None = data.get("event_webhooks_types") - install_params = data.get("install_params") - self.install_params: AppInstallParams | None = ( - AppInstallParams(install_params) if install_params else None + self.install_params: AppInstallParams | None = data.get("install_params") and ( + AppInstallParams(data["install_params"]) ) - self.tags: list[str] | None = data.get("tags", []) + self.tags: list[str] = data.get("tags", []) self.custom_install_url: str | None = data.get("custom_install_url") + self.integration_types_config = IntegrationTypesConfig.from_payload( + data.get("integration_types_config") + ) def __repr__(self) -> str: return ( @@ -235,6 +290,136 @@ def __repr__(self) -> str: f"owner={self.owner!r}>" ) + @property + def flags(self) -> ApplicationFlags | None: + """The public application flags, if set. + + Returns an :class:`ApplicationFlags` instance or ``None`` when not present. + + .. versionadded:: 2.7 + """ + if self._flags is None: + return None + return ApplicationFlags._from_value(self._flags) + + async def edit( + self, + *, + description: str | None = utils.MISSING, + icon: bytes | None = utils.MISSING, + cover_image: bytes | None = utils.MISSING, + tags: list[str] | None = utils.MISSING, + terms_of_service_url: str | None = utils.MISSING, + privacy_policy_url: str | None = utils.MISSING, + interactions_endpoint_url: str | None = utils.MISSING, + role_connections_verification_url: str | None = utils.MISSING, + install_params: AppInstallParams | None = utils.MISSING, + custom_install_url: str | None = utils.MISSING, + integration_types_config: IntegrationTypesConfig | None = utils.MISSING, + flags: ApplicationFlags | None = utils.MISSING, + event_webhooks_url: str | None = utils.MISSING, + event_webhooks_status: bool = utils.MISSING, + event_webhooks_types: list[str] | None = utils.MISSING, + ) -> AppInfo: + """|coro| + + Edit the current application's settings. + + Parameters + ---------- + description: Optional[:class:`str`] + The new application description or ``None`` to clear. + icon: Optional[Union[:class:`bytes`, :class:`str`]] + New icon image. If ``bytes`` is given it will be base64 encoded automatically. If a ``str`` is given it is assumed + to be a pre-encoded base64 data URI or hash and sent as-is. Pass ``None`` to clear. + cover_image: Optional[Union[:class:`bytes`, :class:`str`]] + New cover image for the store embed. If ``bytes`` is given it will be base64 encoded automatically. If a ``str`` is given it is assumed + to be a pre-encoded base64 data URI or hash and sent as-is. Pass ``None`` to clear. + tags: Optional[List[:class:`str`]] + List of tags for the application (max 5). Pass ``None`` to clear. + terms_of_service_url: Optional[:class:`str`] + The application's Terms of Service URL. Pass ``None`` to clear. + privacy_policy_url: Optional[:class:`str`] + The application's Privacy Policy URL. Pass ``None`` to clear. + interactions_endpoint_url: Optional[:class:`str`] + The interactions endpoint callback URL. Pass ``None`` to clear. + role_connections_verification_url: Optional[:class:`str`] + The role connection verification URL for the application. Pass ``None`` to clear. + install_params: Optional[:class:`AppInstallParams`] + Settings for the application's default in-app authorization link. Pass ``None`` to clear. Omit entirely to leave unchanged. + custom_install_url: Optional[:class:`str`] + The default custom authorization URL for the application. Pass ``None`` to clear. + integration_types_config: Optional[:class:`IntegrationTypesConfig`] + Object specifying per-installation context configuration (guild and/or user). You may set contexts individually + and omit others to leave them unchanged. Pass the object with a context explicitly set to ``None`` to clear just that + context, or pass ``None`` to clear the entire integration types configuration. + flags: Optional[:class:`ApplicationFlags`] + Application public flags. Pass ``None`` to clear (not typical). + event_webhooks_url: Optional[:class:`str`] + Event webhooks callback URL for receiving application webhook events. Pass ``None`` to clear. + event_webhooks_status: :class:`bool` + Whether webhook events are enabled. ``True`` maps to API value ``2`` (enabled), ``False`` maps to ``1`` (disabled). + event_webhooks_types: Optional[List[:class:`str`]] + List of webhook event types to subscribe to. Pass ``None`` to clear. + + Returns + ------- + :class:`.AppInfo` + The updated application information. + + .. versionadded:: 2.7 + """ + payload: dict[str, object] = {} + if description is not utils.MISSING: + payload["description"] = description + if icon is not utils.MISSING: + if icon is None: + payload["icon"] = None + else: + payload["icon"] = utils._bytes_to_base64_data(icon) + if cover_image is not utils.MISSING: + if cover_image is None: + payload["cover_image"] = None + else: + payload["cover_image"] = utils._bytes_to_base64_data(cover_image) + if tags is not utils.MISSING: + payload["tags"] = tags + if terms_of_service_url is not utils.MISSING: + payload["terms_of_service_url"] = terms_of_service_url + if privacy_policy_url is not utils.MISSING: + payload["privacy_policy_url"] = privacy_policy_url + if interactions_endpoint_url is not utils.MISSING: + payload["interactions_endpoint_url"] = interactions_endpoint_url + if role_connections_verification_url is not utils.MISSING: + payload["role_connections_verification_url"] = ( + role_connections_verification_url + ) + if install_params is not utils.MISSING: + if install_params is None: + payload["install_params"] = None + else: + payload["install_params"] = install_params.to_payload() + if custom_install_url is not utils.MISSING: + payload["custom_install_url"] = custom_install_url + if integration_types_config is not utils.MISSING: + if integration_types_config is None: + payload["integration_types_config"] = None + else: + payload["integration_types_config"] = ( + integration_types_config.to_payload() + ) + if flags is not utils.MISSING: + payload["flags"] = None if flags is None else flags.value + if event_webhooks_url is not utils.MISSING: + payload["event_webhooks_url"] = event_webhooks_url + if event_webhooks_status is not utils.MISSING: + payload["event_webhooks_status"] = 2 if event_webhooks_status else 1 + if event_webhooks_types is not utils.MISSING: + payload["event_webhooks_types"] = event_webhooks_types + + data = await self._state.http.edit_current_application(payload) + return AppInfo(self._state, data) + @property def icon(self) -> Asset | None: """Retrieves the application's icon asset, if any.""" @@ -278,6 +463,17 @@ def summary(self) -> str | None: ) return self._summary + @property + def event_webhooks_enabled(self) -> bool | None: + """Returns whether event webhooks are enabled. + + This is a convenience around :attr:`event_webhooks_status` where ``True`` means enabled and ``False`` means disabled. + ``None`` indicates the status is not present. + """ + if self.event_webhooks_status is None: + return None + return self.event_webhooks_status == 2 + class PartialAppInfo: """Represents a partial AppInfo given by :func:`~discord.abc.GuildChannel.create_invite` @@ -360,3 +556,83 @@ class AppInstallParams: def __init__(self, data: AppInstallParamsPayload) -> None: self.scopes: list[str] = data.get("scopes", []) self.permissions: Permissions = Permissions(int(data["permissions"])) + + def to_payload(self) -> dict[str, object]: + """Serialize this object into an application install params payload. + + Returns + ------- + Dict[str, Any] + A dict with ``scopes`` and ``permissions`` (string form) suitable for the API. + """ + if self.permissions.value > 0 and "bot" not in self.scopes: + raise ValueError( + "'bot' must be in install_params.scopes if permissions are requested" + ) + return { + "scopes": list(self.scopes), + "permissions": str(self.permissions.value), + } + + +class IntegrationTypesConfig: + """Represents per-installation context configuration for an application. + + This object is used to build the payload for the ``integration_types_config`` field when editing an application. + + Parameters + ---------- + guild: Optional[:class:`AppInstallParams`] + The configuration for the guild installation context. Omit to leave unchanged; pass ``None`` to clear. + user: Optional[:class:`AppInstallParams`] + The configuration for the user installation context. Omit to leave unchanged; pass ``None`` to clear. + """ + + __slots__ = ("guild", "user") + + def __init__( + self, + *, + guild: AppInstallParams | None = utils.MISSING, + user: AppInstallParams | None = utils.MISSING, + ) -> None: + self.guild = guild + self.user = user + + @classmethod + def from_payload(cls, data: dict | None) -> IntegrationTypesConfig | None: + if data is None: + return None + + def _get_ctx(raw: dict, key: int): + if key in raw: + return raw[key] + skey = str(key) + return raw.get(skey) + + def _decode_ctx(value: dict | None) -> AppInstallParams | None: + if value is None: + return None + params = value.get("oauth2_install_params") + if not params: + return None + return AppInstallParams(params) + + guild_ctx = _decode_ctx(_get_ctx(data, 0)) + user_ctx = _decode_ctx(_get_ctx(data, 1)) + return cls(guild=guild_ctx, user=user_ctx) + + def _encode_install_params( + self, value: AppInstallParams | None + ) -> dict[str, object] | None: + if value is None: + return None + return {"oauth2_install_params": value.to_payload()} + + def to_payload(self) -> dict[int, dict[str, object] | None]: + payload: dict[int, dict[str, object] | None] = {} + if self.guild is not utils.MISSING: + payload[0] = self._encode_install_params(self.guild) + if self.user is not utils.MISSING: + payload[1] = self._encode_install_params(self.user) + return payload diff --git a/discord/client.py b/discord/client.py index f339cbfe92..9b949a2781 100644 --- a/discord/client.py +++ b/discord/client.py @@ -59,7 +59,7 @@ from .invite import Invite from .iterators import EntitlementIterator, GuildIterator from .mentions import AllowedMentions -from .monetization import SKU, Entitlement +from .monetization import SKU from .object import Object from .soundboard import SoundboardSound from .stage_instance import StageInstance @@ -77,20 +77,15 @@ if TYPE_CHECKING: from .abc import GuildChannel, PrivateChannel, Snowflake, SnowflakeTime from .channel import ( - CategoryChannel, DMChannel, - ForumChannel, - StageChannel, - TextChannel, - VoiceChannel, ) from .interactions import Interaction from .member import Member from .message import Message from .poll import Poll from .soundboard import SoundboardSound - from .threads import Thread, ThreadMember - from .ui.item import Item, ViewItem + from .threads import Thread + from .ui.item import ViewItem from .voice_client import VoiceProtocol __all__ = ("Client",) @@ -1922,8 +1917,6 @@ async def application_info(self) -> AppInfo: Retrieving the information failed somehow. """ data = await self.http.application_info() - if "rpc_origins" not in data: - data["rpc_origins"] = None return AppInfo(self._connection, data) async def fetch_user(self, user_id: int, /) -> User: diff --git a/discord/http.py b/discord/http.py index ae64703ba6..74cf19c541 100644 --- a/discord/http.py +++ b/discord/http.py @@ -3213,6 +3213,11 @@ def get_answer_voters( def application_info(self) -> Response[appinfo.AppInfo]: return self.request(Route("GET", "/oauth2/applications/@me")) + def edit_current_application( + self, payload: dict[str, Any] + ) -> Response[appinfo.AppInfo]: + return self.request(Route("PATCH", "/applications/@me"), json=payload) + def get_application( self, application_id: Snowflake, / ) -> Response[appinfo.PartialAppInfo]: diff --git a/discord/types/appinfo.py b/discord/types/appinfo.py index c22f665745..8fdccf35f2 100644 --- a/discord/types/appinfo.py +++ b/discord/types/appinfo.py @@ -25,43 +25,71 @@ from __future__ import annotations +from typing import Literal + from typing_extensions import NotRequired, TypedDict +from .guild import Guild from .snowflake import Snowflake from .team import Team -from .user import User +from .user import PartialUser + +ApplicationIntegrationType = Literal[0, 1] +ApplicationEventWebhookStatus = Literal[1, 2, 3] + + +class AppInstallParams(TypedDict): + scopes: list[str] + permissions: str + + +class ApplicationIntegrationTypeConfiguration(TypedDict, total=False): + oauth2_install_params: AppInstallParams class BaseAppInfo(TypedDict): id: Snowflake name: str - verify_key: str - icon: str | None - summary: str description: str - terms_of_service_url: NotRequired[str] - privacy_policy_url: NotRequired[str] - hook: NotRequired[bool] - max_participants: NotRequired[int] - + verify_key: str -class AppInfo(BaseAppInfo): - team: NotRequired[Team] + icon: NotRequired[str | None] + cover_image: NotRequired[str] guild_id: NotRequired[Snowflake] - primary_sku_id: NotRequired[Snowflake] - slug: NotRequired[str] - rpc_origins: list[str] - owner: User - bot_public: bool - bot_require_code_grant: bool + guild: NotRequired[Guild] + bot: NotRequired[PartialUser] + owner: NotRequired[PartialUser] + team: NotRequired[Team | None] + rpc_origins: NotRequired[list[str]] + bot_public: NotRequired[bool] + bot_require_code_grant: NotRequired[bool] + terms_of_service_url: NotRequired[str | None] + privacy_policy_url: NotRequired[str | None] + tags: NotRequired[list[str]] + install_params: NotRequired[AppInstallParams] + custom_install_url: NotRequired[str] + integration_types_config: NotRequired[ + dict[ + ApplicationIntegrationType, + ApplicationIntegrationTypeConfiguration | None, + ] + ] -class PartialAppInfo(BaseAppInfo): - rpc_origins: NotRequired[list[str]] - cover_image: NotRequired[str] - flags: NotRequired[int] +class AppInfo(BaseAppInfo, total=False): + primary_sku_id: Snowflake + slug: str + flags: int + approximate_guild_count: int + approximate_user_install_count: int + approximate_user_authorization_count: int + redirect_uris: list[str] + interactions_endpoint_url: str | None + role_connections_verification_url: str | None + event_webhooks_url: str | None + event_webhooks_status: ApplicationEventWebhookStatus + event_webhooks_types: list[str] -class AppInstallParams(TypedDict): - scopes: list[str] - permissions: str +class PartialAppInfo(BaseAppInfo, total=False): + pass