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: role connections #906

Merged
merged 11 commits into from
Jan 17, 2023
4 changes: 4 additions & 0 deletions changelog/906.feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Add application role connection metadata types.
shiftinv marked this conversation as resolved.
Show resolved Hide resolved
- Add :class:`ApplicationRoleConnectionMetadata` and :class:`ApplicationRoleConnectionMetadataType` types.
- Add :class:`Client.fetch_role_connection_metadata` and :class:`Client.edit_role_connection_metadata` methods.
- Add :attr:`RoleTags.is_linked_role` and :attr:`AppInfo.role_connections_verification_url` attributes.
1 change: 1 addition & 0 deletions disnake/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
from .activity import *
from .app_commands import *
from .appinfo import *
from .application_role_connection import *
from .asset import *
from .audit_logs import *
from .automod import *
Expand Down
10 changes: 10 additions & 0 deletions disnake/appinfo.py
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,12 @@ class AppInfo:
The custom installation url for this application.

.. versionadded:: 2.5
role_connections_verification_url: Optional[:class:`str`]
The application's role connection verification entry point,
which when configured will render the app as a verification method
in the guild role verification configuration.

.. versionadded:: 2.8
"""

__slots__ = (
Expand All @@ -170,6 +176,7 @@ class AppInfo:
"tags",
"install_params",
"custom_install_url",
"role_connections_verification_url",
)

def __init__(self, state: ConnectionState, data: AppInfoPayload) -> None:
Expand Down Expand Up @@ -208,6 +215,9 @@ def __init__(self, state: ConnectionState, data: AppInfoPayload) -> None:
InstallParams(data["install_params"], parent=self) if "install_params" in data else None
)
self.custom_install_url: Optional[str] = data.get("custom_install_url")
self.role_connections_verification_url: Optional[str] = data.get(
"role_connections_verification_url"
)

def __repr__(self) -> str:
return (
Expand Down
112 changes: 112 additions & 0 deletions disnake/application_role_connection.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
# SPDX-License-Identifier: MIT

from __future__ import annotations

from typing import TYPE_CHECKING

from .enums import ApplicationRoleConnectionMetadataType, enum_if_int, try_enum
from .i18n import LocalizationValue, Localized

if TYPE_CHECKING:
from typing_extensions import Self

from .i18n import LocalizationProtocol, LocalizedRequired
from .types.application_role_connection import (
ApplicationRoleConnectionMetadata as ApplicationRoleConnectionMetadataPayload,
)


__all__ = ("ApplicationRoleConnectionMetadata",)


class ApplicationRoleConnectionMetadata:
"""Represents the role connection metadata of an application.

See the :ddocs:`API documentation <resources/application-role-connection-metadata#application-role-connection-metadata-object>`
for further details and limits.

The list of metadata records associated with the current application/bot
can be retrieved/edited using :meth:`Client.fetch_role_connection_metadata`
and :meth:`Client.edit_role_connection_metadata`.

.. versionadded:: 2.8

Attributes
----------
type: :class:`ApplicationRoleConnectionMetadataType`
The type of the metadata value.
key: :class:`str`
The dictionary key for the metadata field.
name: :class:`str`
The name of the metadata field.
name_localizations: :class:`LocalizationValue`
The localizations for :attr:`name`.
description: :class:`str`
The description of the metadata field.
description_localizations: :class:`LocalizationValue`
The localizations for :attr:`description`.
"""

__slots__ = (
"type",
"key",
"name",
"name_localizations",
"description",
"description_localizations",
)

def __init__(
self,
*,
type: ApplicationRoleConnectionMetadataType,
key: str,
name: LocalizedRequired,
description: LocalizedRequired,
) -> None:
self.type: ApplicationRoleConnectionMetadataType = enum_if_int(
ApplicationRoleConnectionMetadataType, type
)
self.key: str = key

name_loc = Localized._cast(name, True)
self.name: str = name_loc.string
self.name_localizations: LocalizationValue = name_loc.localizations

desc_loc = Localized._cast(description, True)
self.description: str = desc_loc.string
self.description_localizations: LocalizationValue = desc_loc.localizations

def __repr__(self) -> str:
return (
f"<ApplicationRoleConnectionMetadata name={self.name!r} key={self.key!r} "
f"description={self.description!r} type={self.type!r}>"
)

@classmethod
def _from_data(cls, data: ApplicationRoleConnectionMetadataPayload) -> Self:
return cls(
type=try_enum(ApplicationRoleConnectionMetadataType, data["type"]),
key=data["key"],
name=Localized(data["name"], data=data.get("name_localizations")),
description=Localized(data["description"], data=data.get("description_localizations")),
)

def to_dict(self) -> ApplicationRoleConnectionMetadataPayload:
data: ApplicationRoleConnectionMetadataPayload = {
"type": self.type.value,
"key": self.key,
"name": self.name,
"description": self.description,
}

if (loc := self.name_localizations.data) is not None:
data["name_localizations"] = loc
if (loc := self.description_localizations.data) is not None:
data["description_localizations"] = loc

return data

def _localize(self, store: LocalizationProtocol) -> None:
self.name_localizations._link(store)
self.description_localizations._link(store)
65 changes: 65 additions & 0 deletions disnake/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
GuildApplicationCommandPermissions,
)
from .appinfo import AppInfo
from .application_role_connection import ApplicationRoleConnectionMetadata
from .backoff import ExponentialBackoff
from .channel import PartialMessageable, _threaded_channel_factory
from .emoji import Emoji
Expand Down Expand Up @@ -81,6 +82,9 @@
from .channel import DMChannel
from .member import Member
from .message import Message
from .types.application_role_connection import (
ApplicationRoleConnectionMetadata as ApplicationRoleConnectionMetadataPayload,
)
from .types.gateway import SessionStartLimit as SessionStartLimitPayload
from .voice_client import VoiceProtocol

Expand Down Expand Up @@ -2687,3 +2691,64 @@ async def fetch_command_permissions(
The permissions configured for the specified application command.
"""
return await self._connection.fetch_command_permissions(guild_id, command_id)

async def fetch_role_connection_metadata(self) -> List[ApplicationRoleConnectionMetadata]:
"""|coro|

Retrieves the :class:`.ApplicationRoleConnectionMetadata` records for the application.

.. versionadded:: 2.8

Raises
------
HTTPException
Retrieving the metadata records failed.

Returns
shiftinv marked this conversation as resolved.
Show resolved Hide resolved
-------
List[:class:`.ApplicationRoleConnectionMetadata`]
The list of metadata records.
"""
data = await self.http.get_application_role_connection_metadata_records(self.application_id)
return [ApplicationRoleConnectionMetadata._from_data(record) for record in data]

async def edit_role_connection_metadata(
self, records: Sequence[ApplicationRoleConnectionMetadata]
) -> List[ApplicationRoleConnectionMetadata]:
"""|coro|
Victorsitou marked this conversation as resolved.
Show resolved Hide resolved

Edits the :class:`.ApplicationRoleConnectionMetadata` records for the application.

An application can have up to 5 metadata records.

.. warning::
This will overwrite all existing metadata records.
Consider :meth:`fetching <fetch_role_connection_metadata>` them first,
and constructing the new list of metadata records based off of the returned list.

.. versionadded:: 2.8

Parameters
----------
records: Sequence[:class:`.ApplicationRoleConnectionMetadata`]
The new metadata records.

Raises
------
HTTPException
Editing the metadata records failed.

Returns
shiftinv marked this conversation as resolved.
Show resolved Hide resolved
-------
List[:class:`.ApplicationRoleConnectionMetadata`]
The list of newly edited metadata records.
"""
payload: List[ApplicationRoleConnectionMetadataPayload] = []
for record in records:
record._localize(self.i18n)
payload.append(record.to_dict())

data = await self.http.edit_application_role_connection_metadata_records(
self.application_id, payload
)
return [ApplicationRoleConnectionMetadata._from_data(record) for record in data]
12 changes: 12 additions & 0 deletions disnake/enums.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@
"AutoModActionType",
"ThreadSortOrder",
"ThreadLayout",
"ApplicationRoleConnectionMetadataType",
)


Expand Down Expand Up @@ -823,6 +824,17 @@ class ThreadLayout(Enum):
gallery_view = 2


class ApplicationRoleConnectionMetadataType(Enum):
integer_less_than_or_equal = 1
integer_greater_than_or_equal = 2
integer_equal = 3
integer_not_equal = 4
datetime_less_than_or_equal = 5
datetime_greater_than_or_equal = 6
boolean_equal = 7
boolean_not_equal = 8


T = TypeVar("T")


Expand Down
26 changes: 26 additions & 0 deletions disnake/http.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
from .message import Attachment
from .types import (
appinfo,
application_role_connection,
audit_log,
automod,
channel,
Expand Down Expand Up @@ -2590,6 +2591,31 @@ def get_voice_regions(self) -> Response[List[voice.VoiceRegion]]:
def application_info(self) -> Response[appinfo.AppInfo]:
return self.request(Route("GET", "/oauth2/applications/@me"))

def get_application_role_connection_metadata_records(
self, application_id: Snowflake
) -> Response[List[application_role_connection.ApplicationRoleConnectionMetadata]]:
return self.request(
Route(
"GET",
"/applications/{application_id}/role-connections/metadata",
application_id=application_id,
)
)

def edit_application_role_connection_metadata_records(
self,
application_id: Snowflake,
records: Sequence[application_role_connection.ApplicationRoleConnectionMetadata],
) -> Response[List[application_role_connection.ApplicationRoleConnectionMetadata]]:
return self.request(
Route(
"PUT",
"/applications/{application_id}/role-connections/metadata",
application_id=application_id,
),
json=records,
)

async def get_gateway(self, *, encoding: str = "json", zlib: bool = True) -> str:
try:
data: gateway.Gateway = await self.request(Route("GET", "/gateway"))
Expand Down
23 changes: 22 additions & 1 deletion disnake/role.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ class RoleTags:
"bot_id",
"integration_id",
"_premium_subscriber",
"_guild_connections",
)

def __init__(self, data: RoleTagPayload) -> None:
Expand All @@ -62,6 +63,7 @@ def __init__(self, data: RoleTagPayload) -> None:
# 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)

def is_bot_managed(self) -> bool:
"""Whether the role is associated with a bot.
Expand All @@ -77,6 +79,15 @@ def is_premium_subscriber(self) -> bool:
"""
return self._premium_subscriber is None

def is_linked_role(self) -> bool:
"""Whether the role is a linked role for the guild.

.. versionadded:: 2.8

:return type: :class:`bool`
"""
return self._guild_connections is None

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

Expand All @@ -87,7 +98,8 @@ def is_integration(self) -> bool:
def __repr__(self) -> str:
return (
f"<RoleTags bot_id={self.bot_id} integration_id={self.integration_id} "
f"premium_subscriber={self.is_premium_subscriber()}>"
f"premium_subscriber={self.is_premium_subscriber()} "
f"linked_role={self.is_linked_role()}>"
)


Expand Down Expand Up @@ -264,6 +276,15 @@ def is_premium_subscriber(self) -> bool:
"""
return self.tags is not None and self.tags.is_premium_subscriber()

def is_linked_role(self) -> bool:
"""Whether the role is a linked role for the guild.

.. versionadded:: 2.8

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

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

Expand Down
1 change: 1 addition & 0 deletions disnake/types/appinfo.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ class AppInfo(BaseAppInfo):
tags: NotRequired[List[str]]
install_params: NotRequired[InstallParams]
custom_install_url: NotRequired[str]
role_connections_verification_url: NotRequired[str]


class PartialAppInfo(BaseAppInfo, total=False):
Expand Down
18 changes: 18 additions & 0 deletions disnake/types/application_role_connection.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# SPDX-License-Identifier: MIT

from typing import Literal, TypedDict

from typing_extensions import NotRequired

from .i18n import LocalizationDict

ApplicationRoleConnectionMetadataType = Literal[1, 2, 3, 4, 5, 6, 7, 8]


class ApplicationRoleConnectionMetadata(TypedDict):
type: ApplicationRoleConnectionMetadataType
key: str
name: str
name_localizations: NotRequired[LocalizationDict]
description: str
description_localizations: NotRequired[LocalizationDict]
5 changes: 5 additions & 0 deletions disnake/types/i18n.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# SPDX-License-Identifier: MIT

from typing import Dict

LocalizationDict = Dict[str, str]
Loading