Repository: hikari-py/hikari
Pull request #1422 (2022-12-25 00:18:22)

Add linked roles support


Patch:
diff --git a/changes/1422.feature.md b/changes/1422.feature.md
new file mode 100644
index 00000000..1a00049f
--- /dev/null
+++ b/changes/1422.feature.md
@@ -0,0 +1,1 @@
+Add linked roles support (models + endpoints).
diff --git a/hikari/__init__.py b/hikari/__init__.py
index 566b3a7d..45a0066f 100644
--- a/hikari/__init__.py
+++ b/hikari/__init__.py
@@ -52,12 +52,15 @@
 from hikari._about import __version__
 from hikari.applications import Application
 from hikari.applications import ApplicationFlags
+from hikari.applications import ApplicationRoleConnectionMetadataRecord
+from hikari.applications import ApplicationRoleConnectionMetadataRecordType
 from hikari.applications import AuthorizationApplication
 from hikari.applications import AuthorizationInformation
 from hikari.applications import ConnectionVisibility
 from hikari.applications import OAuth2AuthorizationToken
 from hikari.applications import OAuth2ImplicitToken
 from hikari.applications import OAuth2Scope
+from hikari.applications import OwnApplicationRoleConnection
 from hikari.applications import OwnConnection
 from hikari.applications import OwnGuild
 from hikari.applications import PartialOAuth2Token
diff --git a/hikari/__init__.pyi b/hikari/__init__.pyi
index 81ae74e0..40b01ca6 100644
--- a/hikari/__init__.pyi
+++ b/hikari/__init__.pyi
@@ -24,12 +24,21 @@ from hikari._about import __url__ as __url__
 from hikari._about import __version__ as __version__
 from hikari.applications import Application as Application
 from hikari.applications import ApplicationFlags as ApplicationFlags
+from hikari.applications import (
+    ApplicationRoleConnectionMetadataRecord as ApplicationRoleConnectionMetadataRecord,
+)
+from hikari.applications import (
+    ApplicationRoleConnectionMetadataRecordType as ApplicationRoleConnectionMetadataRecordType,
+)
 from hikari.applications import AuthorizationApplication as AuthorizationApplication
 from hikari.applications import AuthorizationInformation as AuthorizationInformation
 from hikari.applications import ConnectionVisibility as ConnectionVisibility
 from hikari.applications import OAuth2AuthorizationToken as OAuth2AuthorizationToken
 from hikari.applications import OAuth2ImplicitToken as OAuth2ImplicitToken
 from hikari.applications import OAuth2Scope as OAuth2Scope
+from hikari.applications import (
+    OwnApplicationRoleConnection as OwnApplicationRoleConnection,
+)
 from hikari.applications import OwnConnection as OwnConnection
 from hikari.applications import OwnGuild as OwnGuild
 from hikari.applications import PartialOAuth2Token as PartialOAuth2Token
diff --git a/hikari/api/entity_factory.py b/hikari/api/entity_factory.py
index b0b747df..f2a8e636 100644
--- a/hikari/api/entity_factory.py
+++ b/hikari/api/entity_factory.py
@@ -156,6 +156,23 @@ def deserialize_own_guild(self, payload: data_binding.JSONObject) -> application
             The deserialized "own guild" object.
         """
 
+    @abc.abstractmethod
+    def deserialize_own_application_role_connection(
+        self, payload: data_binding.JSONObject
+    ) -> application_models.OwnApplicationRoleConnection:
+        """Parse a raw payload from Discord into an own application role connection object.
+
+        Parameters
+        ----------
+        payload : hikari.internal.data_binding.JSONObject
+            The JSON payload to deserialize.
+
+        Returns
+        -------
+        hikari.applications.OwnApplicationRoleConnection
+            The deserialized "own application role connection" object.
+        """
+
     @abc.abstractmethod
     def deserialize_application(self, payload: data_binding.JSONObject) -> application_models.Application:
         """Parse a raw payload from Discord into an application object.
@@ -188,6 +205,40 @@ def deserialize_authorization_information(
             The deserialized authorization information object.
         """
 
+    @abc.abstractmethod
+    def deserialize_application_connection_metadata_record(
+        self, payload: data_binding.JSONObject
+    ) -> application_models.ApplicationRoleConnectionMetadataRecord:
+        """Parse a raw payload from Discord into an application connection metadata record object.
+
+        Parameters
+        ----------
+        payload : hikari.internal.data_binding.JSONObject
+            The JSON payload to deserialize.
+
+        Returns
+        -------
+        hikari.applications.ApplicationRoleConnectionMetadataRecord
+            The deserialized "application connection metadata record" object.
+        """
+
+    @abc.abstractmethod
+    def serialize_application_connection_metadata_record(
+        self, record: application_models.ApplicationRoleConnectionMetadataRecord
+    ) -> data_binding.JSONObject:
+        """Serialize an application connection metadata record object to a json serializable dict.
+
+        Parameters
+        ----------
+        record : hikari.applications.ApplicationRoleConnectionMetadataRecord
+            The record object to serialize.
+
+        Returns
+        -------
+        hikari.internal.data_binding.JSONObject
+            The serialized representation of the record object.
+        """
+
     @abc.abstractmethod
     def deserialize_partial_token(self, payload: data_binding.JSONObject) -> application_models.PartialOAuth2Token:
         """Parse a raw payload from Discord into a partial OAuth2 token object.
diff --git a/hikari/api/rest.py b/hikari/api/rest.py
index 32698196..c1631c62 100644
--- a/hikari/api/rest.py
+++ b/hikari/api/rest.py
@@ -2672,6 +2672,112 @@ async def leave_guild(self, guild: snowflakes.SnowflakeishOr[guilds.PartialGuild
             If an internal error occurs on Discord while handling the request.
         """
 
+    @abc.abstractmethod
+    async def fetch_my_user_application_role_connection(
+        self, application: snowflakes.SnowflakeishOr[guilds.PartialApplication]
+    ) -> applications.OwnApplicationRoleConnection:
+        """Fetch the token's associated role connections.
+
+        .. note::
+            This requires the token to have the
+            `hikari.applications.OAuth2Scope.ROLE_CONNECTIONS_WRITE` scope enabled.
+
+        Parameters
+        ----------
+        application : hikari.snowflakes.SnowflakeishOr[hikari.applications.PartialApplication]
+            The application to fetch the application role connections for.
+
+        Returns
+        -------
+        hikari.applications.OwnApplicationRoleConnection
+            The requested role connection.
+
+        Raises
+        ------
+        hikari.errors.UnauthorizedError
+            If you are unauthorized to make the request (invalid/missing token).
+        hikari.errors.NotFoundError
+            If the application is not found.
+        hikari.errors.RateLimitTooLongError
+            Raised in the event that a rate limit occurs that is
+            longer than `max_rate_limit` when making a request.
+        hikari.errors.RateLimitedError
+            Usually, Hikari will handle and retry on hitting
+            rate-limits automatically. This includes most bucket-specific
+            rate-limits and global rate-limits. In some rare edge cases,
+            however, Discord implements other undocumented rules for
+            rate-limiting, such as limits per attribute. These cannot be
+            detected or handled normally by Hikari due to their undocumented
+            nature, and will trigger this exception if they occur.
+        hikari.errors.InternalServerError
+            If an internal error occurs on Discord while handling the request.
+        """
+
+    @abc.abstractmethod
+    async def set_my_user_application_role_connection(
+        self,
+        application: snowflakes.SnowflakeishOr[guilds.PartialApplication],
+        platform_name: undefined.UndefinedOr[str] = undefined.UNDEFINED,
+        platform_username: undefined.UndefinedOr[str] = undefined.UNDEFINED,
+        metadata: undefined.UndefinedOr[
+            typing.Mapping[str, typing.Union[str, int, bool, datetime.datetime]]
+        ] = undefined.UNDEFINED,
+    ) -> applications.OwnApplicationRoleConnection:
+        """Set the token's associated role connections.
+
+        .. note::
+            This requires the token to have the
+            `hikari.applications.OAuth2Scope.ROLE_CONNECTIONS_WRITE` scope enabled.
+
+        Parameters
+        ----------
+        application : hikari.snowflakes.SnowflakeishOr[hikari.applications.PartialApplication]
+            The application to set the application role connections for.
+
+        Other Parameters
+        ----------------
+        platform_name : hikari.undefined.UndefinedOr[str]
+            If provided, the name of the platform that will be connected.
+        platform_username : hikari.undefined.UndefinedOr[str]
+            If provided, the name of the user in the platform.
+        metadata : hikari.undefined.UndefinedOr[typing.Mapping[str, typing.Union[str, int, bool, datetime.datetime]]
+            If provided, the role connection metadata.
+
+            Depending on the time of the previously created application role
+            records through `set_application_role_connection_metadata_records`,
+            this mapping should contain those keys to the valid type of the record:
+                - `INTEGER_X`: An `int`.
+                - `DATETIME_X`: A `datetime.datetime` object.
+                - `BOOLEAN_X`: A `bool`.
+
+        Returns
+        -------
+        hikari.applications.OwnApplicationRoleConnection
+            The set role connection.
+
+        Raises
+        ------
+        hikari.errors.BadRequestError
+            If incorrect values are provided or unknown keys are provided in the metadata.
+        hikari.errors.UnauthorizedError
+            If you are unauthorized to make the request (invalid/missing token).
+        hikari.errors.NotFoundError
+            If the application is not found.
+        hikari.errors.RateLimitTooLongError
+            Raised in the event that a rate limit occurs that is
+            longer than `max_rate_limit` when making a request.
+        hikari.errors.RateLimitedError
+            Usually, Hikari will handle and retry on hitting
+            rate-limits automatically. This includes most bucket-specific
+            rate-limits and global rate-limits. In some rare edge cases,
+            however, Discord implements other undocumented rules for
+            rate-limiting, such as limits per attribute. These cannot be
+            detected or handled normally by Hikari due to their undocumented
+            nature, and will trigger this exception if they occur.
+        hikari.errors.InternalServerError
+            If an internal error occurs on Discord while handling the request.
+        """
+
     @abc.abstractmethod
     async def create_dm_channel(self, user: snowflakes.SnowflakeishOr[users.PartialUser], /) -> channels_.DMChannel:
         """Create a DM channel with a user.
@@ -2750,6 +2856,94 @@ async def fetch_authorization(self) -> applications.AuthorizationInformation:
             If an internal error occurs on Discord while handling the request.
         """
 
+    @abc.abstractmethod
+    async def fetch_application_role_connection_metadata_records(
+        self, application: snowflakes.SnowflakeishOr[guilds.PartialApplication]
+    ) -> typing.Sequence[applications.ApplicationRoleConnectionMetadataRecord]:
+        """Fetch the application role connection metadata records.
+
+        .. note::
+            This requires the token to have the
+            `hikari.applications.OAuth2Scope.ROLE_CONNECTIONS_WRITE` scope enabled.
+
+        Parameters
+        ----------
+        application : hikari.snowflakes.SnowflakeishOr[hikari.applications.PartialApplication]
+            The application to fetch the application role connection metadata records for.
+
+        Returns
+        -------
+        typing.Sequence[hikari.applications.ApplicationRoleConnectionMetadataRecord]
+            The requested application role connection metadata records.
+
+        Raises
+        ------
+        hikari.errors.UnauthorizedError
+            If you are unauthorized to make the request (invalid/missing token).
+        hikari.errors.NotFoundError
+            If the application is not found.
+        hikari.errors.RateLimitTooLongError
+            Raised in the event that a rate limit occurs that is
+            longer than `max_rate_limit` when making a request.
+        hikari.errors.RateLimitedError
+            Usually, Hikari will handle and retry on hitting
+            rate-limits automatically. This includes most bucket-specific
+            rate-limits and global rate-limits. In some rare edge cases,
+            however, Discord implements other undocumented rules for
+            rate-limiting, such as limits per attribute. These cannot be
+            detected or handled normally by Hikari due to their undocumented
+            nature, and will trigger this exception if they occur.
+        hikari.errors.InternalServerError
+            If an internal error occurs on Discord while handling the request.
+        """
+
+    @abc.abstractmethod
+    async def set_application_role_connection_metadata_records(
+        self,
+        application: snowflakes.SnowflakeishOr[guilds.PartialApplication],
+        records: typing.Sequence[applications.ApplicationRoleConnectionMetadataRecord],
+    ) -> typing.Sequence[applications.ApplicationRoleConnectionMetadataRecord]:
+        """Set the application role connection metadata records.
+
+        .. note::
+            This requires the token to have the
+            `hikari.applications.OAuth2Scope.ROLE_CONNECTIONS_WRITE` scope enabled.
+
+        Parameters
+        ----------
+        application : hikari.snowflakes.SnowflakeishOr[hikari.applications.PartialApplication]
+            The application to set the application role connection metadata records for.
+        records : typing.Sequence[hikari.applications.ApplicationRoleConnectionMetadataRecord]
+            The records to set for the application.
+
+        Returns
+        -------
+        typing.Sequence[hikari.applications.ApplicationRoleConnectionMetadataRecord]
+            The set application role connection metadata records.
+
+        Raises
+        ------
+        hikari.errors.BadRequestError
+            If incorrect values are provided for the records.
+        hikari.errors.UnauthorizedError
+            If you are unauthorized to make the request (invalid/missing token).
+        hikari.errors.NotFoundError
+            If the application is not found.
+        hikari.errors.RateLimitTooLongError
+            Raised in the event that a rate limit occurs that is
+            longer than `max_rate_limit` when making a request.
+        hikari.errors.RateLimitedError
+            Usually, Hikari will handle and retry on hitting
+            rate-limits automatically. This includes most bucket-specific
+            rate-limits and global rate-limits. In some rare edge cases,
+            however, Discord implements other undocumented rules for
+            rate-limiting, such as limits per attribute. These cannot be
+            detected or handled normally by Hikari due to their undocumented
+            nature, and will trigger this exception if they occur.
+        hikari.errors.InternalServerError
+            If an internal error occurs on Discord while handling the request.
+        """
+
     @abc.abstractmethod
     async def authorize_client_credentials_token(
         self,
diff --git a/hikari/applications.py b/hikari/applications.py
index f3b045c6..d7a3f171 100644
--- a/hikari/applications.py
+++ b/hikari/applications.py
@@ -36,11 +36,14 @@
     "OAuth2Scope",
     "OwnConnection",
     "OwnGuild",
+    "OwnApplicationRoleConnection",
     "PartialOAuth2Token",
     "Team",
     "TeamMember",
     "TeamMembershipState",
     "TokenType",
+    "ApplicationRoleConnectionMetadataRecordType",
+    "ApplicationRoleConnectionMetadataRecord",
     "get_token_id",
 )
 
@@ -50,6 +53,7 @@
 import attr
 
 from hikari import guilds
+from hikari import locales
 from hikari import snowflakes
 from hikari import urls
 from hikari import users
@@ -236,6 +240,9 @@ class OAuth2Scope(str, enums.Enum):
     GUILDS_MEMBERS_READ = "guilds.members.read"
     """Used to read the current user's guild members."""
 
+    ROLE_CONNECTIONS_WRITE = "role_connections.write"
+    """Used to write to the current user's connection and metadata for the app."""
+
 
 @typing.final
 class ConnectionVisibility(int, enums.Enum):
@@ -302,6 +309,30 @@ class OwnGuild(guilds.PartialGuild):
     """The guild-level permissions that apply to the current user or bot."""
 
 
+@attr.define(hash=True, kw_only=True, weakref_slot=False)
+class OwnApplicationRoleConnection:
+    """Represents an own application role connection."""
+
+    platform_name: typing.Optional[str] = attr.field(eq=True, hash=True, repr=True)
+    """The name of the platform."""
+
+    platform_username: typing.Optional[str] = attr.field(eq=True, hash=True, repr=True)
+    """The users name in the platform."""
+
+    metadata: typing.Mapping[str, str] = attr.field(eq=False, hash=False, repr=False)
+    """Mapping application role connection metadata keys to their value.
+
+    .. note::
+        Unfortunately, these can't be deserialized to their proper types as Discord don't
+        provide a way to difference between them.
+
+        You can deserialize them yourself based on what value you expect from the key:
+            - `INTEGER_X`: Cast to an `int`.
+            - `DATETIME_X`: Cast to a `datetime.datetime.fromisoformat` or `ciso8601.parse_rfc3339` for speed.
+            - `BOOLEAN_X`: Cast to a `bool`.
+    """
+
+
 @typing.final
 class TeamMembershipState(int, enums.Enum):
     """Represents the state of a user's team membership."""
@@ -587,6 +618,9 @@ class Application(guilds.PartialApplication):
     privacy_policy_url: typing.Optional[str] = attr.field(eq=False, hash=False, repr=False)
     """The URL of this application's privacy policy."""
 
+    role_connections_verification_url: typing.Optional[str] = attr.field(eq=False, hash=False, repr=False)
+    """The URL of this application's role connection verification entry point."""
+
     custom_install_url: typing.Optional[str] = attr.field(eq=False, hash=False, repr=False)
     """The URL of this application's custom authorization link."""
 
@@ -752,6 +786,68 @@ class TokenType(str, enums.Enum):
     """OAuth2 bearer token type."""
 
 
+@typing.final
+class ApplicationRoleConnectionMetadataRecordType(int, enums.Enum):
+    """Represents possible application role connection metadata record types."""
+
+    INTEGER_LESS_THAN_OR_EQUAL = 1
+    """Integer Less Than Or Equal."""
+
+    INTEGER_GREATER_THAN_OR_EQUAL = 2
+    """Integer Greater Than Or Equal."""
+
+    INTEGER_EQUAL = 3
+    """Integer Equal."""
+
+    INTEGER_NOT_EQUAL = 4
+    """Integer Not Equal."""
+
+    DATETIME_LESS_THAN_OR_EQUAL = 5
+    """Datetime Less Than Or Equal."""
+
+    DATETIME_GREATER_THAN_OR_EQUAL = 6
+    """Datetime Greater Than Or Equal."""
+
+    BOOLEAN_EQUAL = 7
+    """Boolean Equal."""
+
+    BOOLEAN_NOT_EQUAL = 8
+    """Boolean Not Equal."""
+
+
+@attr.define(hash=True, kw_only=True, weakref_slot=False)
+class ApplicationRoleConnectionMetadataRecord:
+    """Represents a role connection metadata record."""
+
+    type: typing.Union[ApplicationRoleConnectionMetadataRecordType, int] = attr.field(eq=False, hash=False, repr=False)
+    """The type of metadata value record."""
+
+    key: str = attr.field(eq=True, hash=True, repr=False)
+    """Dictionary key for the metadata field."""
+
+    name: str = attr.field(eq=False, hash=False, repr=True)
+    """The metadata's field name."""
+
+    description: str = attr.field(eq=False, hash=False, repr=True)
+    """The metadata's field description."""
+
+    name_localizations: typing.Mapping[typing.Union[locales.Locale, str], str] = attr.field(
+        eq=False,
+        hash=False,
+        repr=False,
+        factory=dict,
+    )
+    """A mapping of name localizations for this metadata field."""
+
+    description_localizations: typing.Mapping[typing.Union[locales.Locale, str], str] = attr.field(
+        eq=False,
+        hash=False,
+        repr=False,
+        factory=dict,
+    )
+    """A mapping of description localizations for this metadata field."""
+
+
 def get_token_id(token: str) -> snowflakes.Snowflake:
     """Try to get the bot ID stored in a token.
 
diff --git a/hikari/commands.py b/hikari/commands.py
index 63b59162..3a2cb76c 100644
--- a/hikari/commands.py
+++ b/hikari/commands.py
@@ -187,12 +187,12 @@ class CommandOption:
     name_localizations: typing.Mapping[typing.Union[locales.Locale, str], str] = attr.field(
         eq=False, factory=dict, hash=False, repr=False
     )
-    """A set of name localizations for this option."""
+    """A mapping of name localizations for this option."""
 
     description_localizations: typing.Mapping[typing.Union[locales.Locale, str], str] = attr.field(
         eq=False, factory=dict, hash=False, repr=False
     )
-    """A set of description localizations for this option"""
+    """A mapping of description localizations for this option."""
 
     min_length: typing.Optional[int] = attr.field(default=None, repr=False)
     """The minimum length permitted (inclusive).
@@ -254,9 +254,9 @@ class PartialCommand(snowflakes.Unique):
     """Auto-incrementing version identifier updated during substantial record changes."""
 
     name_localizations: typing.Mapping[typing.Union[locales.Locale, str], str] = attr.field(
-        eq=False, hash=False, repr=False
+        eq=False, factory=dict, hash=False, repr=False
     )
-    """A set of name localizations for this command"""
+    """A mapping of name localizations for this option."""
 
     async def fetch_self(self) -> PartialCommand:
         """Fetch an up-to-date version of this command object.
diff --git a/hikari/guilds.py b/hikari/guilds.py
index a80a5b77..7eab63be 100644
--- a/hikari/guilds.py
+++ b/hikari/guilds.py
@@ -976,7 +976,7 @@ def __str__(self) -> str:
 
 @attr.define(hash=True, kw_only=True, weakref_slot=False)
 class Role(PartialRole):
-    """Represents a guild bound Role object."""
+    """Represents a guild bound role object."""
 
     color: colors.Color = attr.field(eq=False, hash=False, repr=True)
     """The colour of this role.
@@ -1084,10 +1084,13 @@ class IntegrationType(str, enums.Enum):
     """The integration type."""
 
     TWITCH = "twitch"
+    "Twitch."
 
     YOUTUBE = "youtube"
+    "Youtube."
 
     DISCORD_BOT = "discord"
+    "Discord bot."
 
 
 @typing.final
diff --git a/hikari/impl/entity_factory.py b/hikari/impl/entity_factory.py
index c60c933a..b5c5c332 100644
--- a/hikari/impl/entity_factory.py
+++ b/hikari/impl/entity_factory.py
@@ -588,6 +588,15 @@ def deserialize_own_guild(self, payload: data_binding.JSONObject) -> application
             my_permissions=permission_models.Permissions(int(payload["permissions"])),
         )
 
+    def deserialize_own_application_role_connection(
+        self, payload: data_binding.JSONObject
+    ) -> application_models.OwnApplicationRoleConnection:
+        return application_models.OwnApplicationRoleConnection(
+            platform_name=payload.get("platform_name"),
+            platform_username=payload.get("platform_username"),
+            metadata=payload.get("metadata") or {},
+        )
+
     def deserialize_application(self, payload: data_binding.JSONObject) -> application_models.Application:
         team: typing.Optional[application_models.Team] = None
         if (team_payload := payload.get("team")) is not None:
@@ -633,6 +642,7 @@ def deserialize_application(self, payload: data_binding.JSONObject) -> applicati
             cover_image_hash=payload.get("cover_image"),
             privacy_policy_url=payload.get("privacy_policy_url"),
             terms_of_service_url=payload.get("terms_of_service_url"),
+            role_connections_verification_url=payload.get("role_connections_verification_url"),
             custom_install_url=payload.get("custom_install_url"),
             tags=payload.get("tags") or [],
             install_parameters=install_parameters,
@@ -661,6 +671,44 @@ def deserialize_authorization_information(
             user=self.deserialize_user(payload["user"]) if "user" in payload else None,
         )
 
+    def deserialize_application_connection_metadata_record(
+        self, payload: data_binding.JSONObject
+    ) -> application_models.ApplicationRoleConnectionMetadataRecord:
+        name_localizations: typing.Mapping[str, str]
+        if raw_name_localizations := payload.get("name_localizations"):
+            name_localizations = {locales.Locale(k): raw_name_localizations[k] for k in raw_name_localizations}
+        else:
+            name_localizations = {}
+
+        description_localizations: typing.Mapping[str, str]
+        if raw_description_localizations := payload.get("description_localizations"):
+            description_localizations = {
+                locales.Locale(k): raw_description_localizations[k] for k in raw_description_localizations
+            }
+        else:
+            description_localizations = {}
+
+        return application_models.ApplicationRoleConnectionMetadataRecord(
+            type=application_models.ApplicationRoleConnectionMetadataRecordType(payload["type"]),
+            key=payload["key"],
+            name=payload["name"],
+            description=payload["description"],
+            name_localizations=name_localizations,
+            description_localizations=description_localizations,
+        )
+
+    def serialize_application_connection_metadata_record(
+        self, record: application_models.ApplicationRoleConnectionMetadataRecord
+    ) -> data_binding.JSONObject:
+        return {
+            "type": int(record.type),
+            "key": record.key,
+            "name": record.name,
+            "description": record.description,
+            "name_localizations": record.name_localizations,
+            "description_localizations": record.description_localizations,
+        }
+
     def deserialize_partial_token(self, payload: data_binding.JSONObject) -> application_models.PartialOAuth2Token:
         return application_models.PartialOAuth2Token(
             access_token=payload["access_token"],
diff --git a/hikari/impl/rest.py b/hikari/impl/rest.py
index 2d9c26e3..6123645a 100644
--- a/hikari/impl/rest.py
+++ b/hikari/impl/rest.py
@@ -2080,6 +2080,45 @@ async def leave_guild(self, guild: snowflakes.SnowflakeishOr[guilds.PartialGuild
         route = routes.DELETE_MY_GUILD.compile(guild=guild)
         await self._request(route)
 
+    async def fetch_my_user_application_role_connection(
+        self, application: snowflakes.SnowflakeishOr[guilds.PartialApplication]
+    ) -> applications.OwnApplicationRoleConnection:
+        route = routes.GET_MY_USER_APPLICATION_ROLE_CONNECTIONS.compile(application=application)
+
+        response = await self._request(route)
+        assert isinstance(response, dict)
+        return self._entity_factory.deserialize_own_application_role_connection(response)
+
+    async def set_my_user_application_role_connection(
+        self,
+        application: snowflakes.SnowflakeishOr[guilds.PartialApplication],
+        platform_name: undefined.UndefinedOr[str] = undefined.UNDEFINED,
+        platform_username: undefined.UndefinedOr[str] = undefined.UNDEFINED,
+        metadata: undefined.UndefinedOr[
+            typing.Mapping[str, typing.Union[str, int, bool, datetime.datetime]]
+        ] = undefined.UNDEFINED,
+    ) -> applications.OwnApplicationRoleConnection:
+        route = routes.PUT_MY_USER_APPLICATION_ROLE_CONNECTIONS.compile(application=application)
+        body = data_binding.JSONObjectBuilder()
+        body.put("platform_name", platform_name)
+        body.put("platform_username", platform_username)
+
+        if metadata is not undefined.UNDEFINED:
+            # Syntactic sugar for metadata to allow booleans and datetime.datetime
+            metadata = dict(metadata)
+
+            for key, value in metadata.items():
+                if isinstance(value, bool):
+                    metadata[key] = int(value)
+                elif isinstance(value, datetime.datetime):
+                    metadata[key] = value.isoformat()
+
+            body.put("metadata", metadata)
+
+        response = await self._request(route, json=body)
+        assert isinstance(response, dict)
+        return self._entity_factory.deserialize_own_application_role_connection(response)
+
     async def create_dm_channel(self, user: snowflakes.SnowflakeishOr[users.PartialUser], /) -> channels_.DMChannel:
         route = routes.POST_MY_CHANNELS.compile()
         body = data_binding.JSONObjectBuilder()
@@ -2105,6 +2144,28 @@ async def fetch_authorization(self) -> applications.AuthorizationInformation:
         assert isinstance(response, dict)
         return self._entity_factory.deserialize_authorization_information(response)
 
+    async def fetch_application_role_connection_metadata_records(
+        self, application: snowflakes.SnowflakeishOr[guilds.PartialApplication]
+    ) -> typing.Sequence[applications.ApplicationRoleConnectionMetadataRecord]:
+        route = routes.GET_APPLICATION_ROLE_CONNECTION_METADATA_RECORDS.compile(application=application)
+
+        response = await self._request(route)
+        assert isinstance(response, list)
+        return [self._entity_factory.deserialize_application_connection_metadata_record(r) for r in response]
+
+    async def set_application_role_connection_metadata_records(
+        self,
+        application: snowflakes.SnowflakeishOr[guilds.PartialApplication],
+        records: typing.Sequence[applications.ApplicationRoleConnectionMetadataRecord],
+    ) -> typing.Sequence[applications.ApplicationRoleConnectionMetadataRecord]:
+        route = routes.PUT_APPLICATION_ROLE_CONNECTION_METADATA_RECORDS.compile(application=application)
+
+        body = [self._entity_factory.serialize_application_connection_metadata_record(r) for r in records]
+
+        response = await self._request(route, json=body)
+        assert isinstance(response, list)
+        return [self._entity_factory.deserialize_application_connection_metadata_record(r) for r in response]
+
     @staticmethod
     def _gen_oauth2_token(client: snowflakes.SnowflakeishOr[guilds.PartialApplication], client_secret: str) -> str:
         token = base64.b64encode(f"{int(client)}:{client_secret}".encode()).decode("utf-8")
diff --git a/hikari/impl/special_endpoints.py b/hikari/impl/special_endpoints.py
index 3d9bb275..92175a2b 100644
--- a/hikari/impl/special_endpoints.py
+++ b/hikari/impl/special_endpoints.py
@@ -1907,7 +1907,9 @@ class ModalActionRowBuilder(special_endpoints.ModalActionRowBuilder):
     """Standard implementation of `hikari.api.special_endpoints.ActionRowBuilder`."""
 
     _components: typing.List[special_endpoints.ComponentBuilder] = attr.field(alias="components", factory=list)
-    _stored_type: typing.Optional[component_models.ComponentType] = attr.field(alias="stored_type", default=None)
+    _stored_type: typing.Optional[component_models.ComponentType] = attr.field(
+        alias="stored_type", init=False, default=None
+    )
 
     @property
     def components(self) -> typing.Sequence[special_endpoints.ComponentBuilder]:
diff --git a/hikari/internal/routes.py b/hikari/internal/routes.py
index a082971c..9b70248f 100644
--- a/hikari/internal/routes.py
+++ b/hikari/internal/routes.py
@@ -469,9 +469,15 @@ def compile_to_file(
 
 # @me
 POST_MY_CHANNELS: typing.Final[Route] = Route(POST, "/users/@me/channels")
-GET_MY_CONNECTIONS: typing.Final[Route] = Route(GET, "/users/@me/connections")  # OAuth2 only
-GET_MY_GUILD_MEMBER: typing.Final[Route] = Route(GET, "/users/@me/guilds/{guild}/member")  # OAuth2 only
+GET_MY_CONNECTIONS: typing.Final[Route] = Route(GET, "/users/@me/connections")  # OAuth2
+GET_MY_GUILD_MEMBER: typing.Final[Route] = Route(GET, "/users/@me/guilds/{guild}/member")  # OAuth2
 DELETE_MY_GUILD: typing.Final[Route] = Route(DELETE, "/users/@me/guilds/{guild}")
+GET_MY_USER_APPLICATION_ROLE_CONNECTIONS: typing.Final[Route] = Route(
+    GET, "/users/@me/applications/{application}/role-connection"
+)  # OAuth2
+PUT_MY_USER_APPLICATION_ROLE_CONNECTIONS: typing.Final[Route] = Route(
+    PUT, "/users/@me/applications/{application}/role-connection"
+)  # OAuth2
 
 GET_MY_GUILDS: typing.Final[Route] = Route(GET, "/users/@me/guilds")
 
@@ -531,8 +537,12 @@ def compile_to_file(
 PUT_APPLICATION_COMMAND_PERMISSIONS: typing.Final[Route] = Route(
     PUT, "/applications/{application}/guilds/{guild}/commands/{command}/permissions"
 )
-PUT_APPLICATION_GUILD_COMMANDS_PERMISSIONS: typing.Final[Route] = Route(
-    PUT, "/applications/{application}/guilds/{guild}/commands/permissions"
+
+GET_APPLICATION_ROLE_CONNECTION_METADATA_RECORDS: typing.Final[Route] = Route(
+    GET, "/applications/{application}/role-connections/metadata"
+)
+PUT_APPLICATION_ROLE_CONNECTION_METADATA_RECORDS: typing.Final[Route] = Route(
+    PUT, "/applications/{application}/role-connections/metadata"
 )
 
 # Interactions


Test patch:
diff --git a/tests/hikari/impl/test_entity_factory.py b/tests/hikari/impl/test_entity_factory.py
index 8c48f291..358129b1 100644
--- a/tests/hikari/impl/test_entity_factory.py
+++ b/tests/hikari/impl/test_entity_factory.py
@@ -920,6 +920,28 @@ def test_deserialize_own_guild_with_null_and_unset_fields(self, entity_factory_i
         )
         assert own_guild.icon_hash is None
 
+    @pytest.fixture()
+    def role_connection_payload(self):
+        return {
+            "platform_name": "Muck",
+            "platform_username": "Muck Muck Muck",
+            "metadata": {
+                "key": "value",
+                "key2": "value2",
+            },
+        }
+
+    def test_deserialize_own_application_role_connection(self, entity_factory_impl, role_connection_payload):
+        role_connection = entity_factory_impl.deserialize_own_application_role_connection(role_connection_payload)
+
+        assert role_connection.platform_name == "Muck"
+        assert role_connection.platform_username == "Muck Muck Muck"
+        assert role_connection.metadata == {
+            "key": "value",
+            "key2": "value2",
+        }
+        assert isinstance(role_connection, application_models.OwnApplicationRoleConnection)
+
     @pytest.fixture()
     def owner_payload(self, user_payload):
         return {**user_payload, "flags": 1 << 10}
@@ -949,6 +971,7 @@ def application_payload(self, owner_payload, user_payload):
             "cover_image": "hashmebaby",
             "privacy_policy_url": "hahaha://hahaha",
             "terms_of_service_url": "haha2:2h2h2h2",
+            "role_connections_verification_url": "https://verifymethis.com",
             "custom_install_url": "https://dontinstallme.com",
             "tags": ["i", "like", "hikari"],
             "install_params": {
@@ -977,6 +1000,7 @@ def test_deserialize_application(
         assert application.flags == application_models.ApplicationFlags.VERIFICATION_PENDING_GUILD_LIMIT
         assert application.privacy_policy_url == "hahaha://hahaha"
         assert application.terms_of_service_url == "haha2:2h2h2h2"
+        assert application.role_connections_verification_url == "https://verifymethis.com"
         assert application.custom_install_url == "https://dontinstallme.com"
         assert application.tags == ["i", "like", "hikari"]
         assert application.icon_hash == "iwiwiwiwiw"
@@ -1025,6 +1049,7 @@ def test_deserialize_application_with_unset_fields(self, entity_factory_impl, mo
         assert application.cover_image_hash is None
         assert application.privacy_policy_url is None
         assert application.terms_of_service_url is None
+        assert application.role_connections_verification_url is None
 
     def test_deserialize_application_with_null_fields(self, entity_factory_impl, mock_app, owner_payload):
         application = entity_factory_impl.deserialize_application(
@@ -1047,6 +1072,7 @@ def test_deserialize_application_with_null_fields(self, entity_factory_impl, moc
         assert application.terms_of_service_url is None
         assert application.privacy_policy_url is None
         assert application.custom_install_url is None
+        assert application.role_connections_verification_url is None
         assert application.cover_image_hash is None
         assert application.team is None
         assert application.install_parameters is None
@@ -1128,6 +1154,85 @@ def test_deserialize_authorization_information_with_unset_fields(
         assert authorization_information.application.terms_of_service_url is None
         assert authorization_information.application.privacy_policy_url is None
 
+    @pytest.fixture()
+    def application_connection_metadata_record_payload(self):
+        return {
+            "type": 7,
+            "key": "developer_value",
+            "name": "A thing",
+            "description": "Description of the thing",
+            "name_localizations": {
+                "en-UK": "A thing (but in Bri'ish)",
+                "es": "Una cosa",
+            },
+            "description_localizations": {
+                "en-UK": "Description of the thing (but in Bri'ish)",
+                "es": "Descripción de la cosa",
+            },
+        }
+
+    def test_deserialize_application_connection_metadata_record(
+        self, entity_factory_impl, application_connection_metadata_record_payload
+    ):
+        record = entity_factory_impl.deserialize_application_connection_metadata_record(
+            application_connection_metadata_record_payload
+        )
+
+        assert record.type == application_models.ApplicationRoleConnectionMetadataRecordType.BOOLEAN_EQUAL
+        assert record.key == "developer_value"
+        assert record.name == "A thing"
+        assert record.description == "Description of the thing"
+        assert record.name_localizations == {
+            "en-UK": "A thing (but in Bri'ish)",
+            "es": "Una cosa",
+        }
+        assert record.description_localizations == {
+            "en-UK": "Description of the thing (but in Bri'ish)",
+            "es": "Descripción de la cosa",
+        }
+
+    def test_deserialize_application_connection_metadata_record_with_missing_fields(
+        self, entity_factory_impl, application_connection_metadata_record_payload
+    ):
+        del application_connection_metadata_record_payload["name_localizations"]
+        del application_connection_metadata_record_payload["description_localizations"]
+
+        record = entity_factory_impl.deserialize_application_connection_metadata_record(
+            application_connection_metadata_record_payload
+        )
+
+        assert record.name_localizations == {}
+        assert record.description_localizations == {}
+
+    def test_serialize_application_connection_metadata_record(self, entity_factory_impl):
+        record = application_models.ApplicationRoleConnectionMetadataRecord(
+            type=application_models.ApplicationRoleConnectionMetadataRecordType.BOOLEAN_EQUAL,
+            key="some_key",
+            name="Testing this out",
+            description="Describing this out",
+            name_localizations={
+                "some_language": "Its name localization",
+            },
+            description_localizations={
+                "some_other_language": "Its description localization",
+            },
+        )
+
+        expected_result = {
+            "type": 7,
+            "key": "some_key",
+            "name": "Testing this out",
+            "description": "Describing this out",
+            "name_localizations": {
+                "some_language": "Its name localization",
+            },
+            "description_localizations": {
+                "some_other_language": "Its description localization",
+            },
+        }
+
+        assert entity_factory_impl.serialize_application_connection_metadata_record(record) == expected_result
+
     @pytest.fixture()
     def client_credentials_payload(self):
         return {