From a1caa38bfd5bdd9a394df3ae40feb233f53baff5 Mon Sep 17 00:00:00 2001 From: kozabrada123 Date: Tue, 13 Aug 2024 08:43:37 +0200 Subject: [PATCH] feat: add rest* of Connections api * The only thing not added yet is create_domain_connection, because it uses errors in a funky way adds: - create_connection_callback - create_contact_sync_connection - get_connections - refresh_connection - modify_connection - delete_connection - get_connection_access_token - get_connection_subreddits + related schema for all those routes, and some supporting types --- src/api/users/connections.rs | 258 ++++++++++++++++++++++++++++++- src/gateway/events.rs | 1 + src/gateway/gateway.rs | 1 + src/types/entities/connection.rs | 52 ++++++- src/types/events/user.rs | 10 ++ src/types/schema/user.rs | 77 +++++++++ 6 files changed, 394 insertions(+), 5 deletions(-) diff --git a/src/api/users/connections.rs b/src/api/users/connections.rs index 2fd9580..58ed5d2 100644 --- a/src/api/users/connections.rs +++ b/src/api/users/connections.rs @@ -1,15 +1,26 @@ +use futures_util::FutureExt; use reqwest::Client; use crate::{ errors::ChorusResult, instance::ChorusUser, ratelimiter::ChorusRequest, - types::{AuthorizeConnectionReturn, AuthorizeConnectionSchema, ConnectionType, LimitType}, + types::{ + AuthorizeConnectionReturn, AuthorizeConnectionSchema, Connection, ConnectionSubreddit, + ConnectionType, CreateConnectionCallbackSchema, CreateContactSyncConnectionSchema, + GetConnectionAccessTokenReturn, LimitType, ModifyConnectionSchema, + }, }; impl ChorusUser { /// Fetches a url that can be used for authorizing a new connection. /// + /// The user should then visit the url and authenticate to create the connection. + /// + /// # Notes + /// This route seems to be preferred by the official infrastructure (client) to + /// [Self::create_connection_callback]. + /// /// # Reference /// See /// @@ -44,4 +55,249 @@ impl ChorusUser { .await .map(|response| response.url) } + + /// Creates a new connection for the current user. + /// + /// # Notes + /// The official infrastructure (client) prefers the route + /// [Self::authorize_connection] to this one. + /// + /// # Reference + /// See + // TODO: When is this called? When should it be used over authorize_connection? + pub async fn create_connection_callback( + &mut self, + connection_type: ConnectionType, + json_schema: CreateConnectionCallbackSchema, + ) -> ChorusResult { + let connection_type_string = serde_json::to_string(&connection_type) + .expect("Failed to serialize connection type!") + .replace('"', ""); + + let request = Client::new() + .post(format!( + "{}/connections/{}/callback", + self.belongs_to.read().unwrap().urls.api, + connection_type_string + )) + .header("Authorization", self.token()) + .json(&json_schema); + + let chorus_request = ChorusRequest { + request, + limit_type: LimitType::default(), + }; + + chorus_request.deserialize_response(self).await + } + + /// Creates a new contact sync connection for the current user. + /// + /// # Notes + /// To create normal connection types, see [Self::authorize_connection] and + /// [Self::create_connection_callback] + /// + /// # Reference + /// See + pub async fn create_contact_sync_connection( + &mut self, + connection_account_id: &String, + json_schema: CreateContactSyncConnectionSchema, + ) -> ChorusResult { + let request = Client::new() + .put(format!( + "{}/users/@me/connections/contacts/{}", + self.belongs_to.read().unwrap().urls.api, + connection_account_id + )) + .header("Authorization", self.token()) + .json(&json_schema); + + let chorus_request = ChorusRequest { + request, + limit_type: LimitType::default(), + }; + + chorus_request.deserialize_response(self).await + } + + // TODO: Add create_domain_connection () + // It requires changing how chorus handles errors to support properly + + /// Fetches the current user's [Connection]s + /// + /// # Reference + /// See + pub async fn get_connections(&mut self) -> ChorusResult> { + let request = Client::new() + .get(format!( + "{}/users/@me/connections", + self.belongs_to.read().unwrap().urls.api, + )) + .header("Authorization", self.token()); + + let chorus_request = ChorusRequest { + request, + limit_type: LimitType::default(), + }; + + chorus_request.deserialize_response(self).await + } + + /// Refreshes a local user's [Connection]. + /// + /// # Reference + /// See + pub async fn refresh_connection( + &mut self, + connection_type: ConnectionType, + connection_account_id: &String, + ) -> ChorusResult<()> { + let connection_type_string = serde_json::to_string(&connection_type) + .expect("Failed to serialize connection type!") + .replace('"', ""); + + let request = Client::new() + .post(format!( + "{}/users/@me/connections/{}/{}/refresh", + self.belongs_to.read().unwrap().urls.api, + connection_type_string, + connection_account_id + )) + .header("Authorization", self.token()); + + let chorus_request = ChorusRequest { + request, + limit_type: LimitType::default(), + }; + + chorus_request.handle_request_as_result(self).await + } + + /// Changes settings on a local user's [Connection]. + /// + /// # Notes + /// Not all connection types support all parameters. + /// + /// # Reference + /// See + pub async fn modify_connection( + &mut self, + connection_type: ConnectionType, + connection_account_id: &String, + json_schema: ModifyConnectionSchema, + ) -> ChorusResult { + let connection_type_string = serde_json::to_string(&connection_type) + .expect("Failed to serialize connection type!") + .replace('"', ""); + + let request = Client::new() + .patch(format!( + "{}/users/@me/connections/{}/{}", + self.belongs_to.read().unwrap().urls.api, + connection_type_string, + connection_account_id + )) + .header("Authorization", self.token()) + .json(&json_schema); + + let chorus_request = ChorusRequest { + request, + limit_type: LimitType::default(), + }; + + chorus_request.deserialize_response(self).await + } + + /// Deletes a local user's [Connection]. + /// + /// # Reference + /// See + pub async fn delete_connection( + &mut self, + connection_type: ConnectionType, + connection_account_id: &String, + ) -> ChorusResult<()> { + let connection_type_string = serde_json::to_string(&connection_type) + .expect("Failed to serialize connection type!") + .replace('"', ""); + + let request = Client::new() + .delete(format!( + "{}/users/@me/connections/{}/{}", + self.belongs_to.read().unwrap().urls.api, + connection_type_string, + connection_account_id + )) + .header("Authorization", self.token()); + + let chorus_request = ChorusRequest { + request, + limit_type: LimitType::default(), + }; + + chorus_request.handle_request_as_result(self).await + } + + /// Returns a new access token for the given connection. + /// + /// Only available for [ConnectionType::Twitch], [ConnectionType::YouTube] and [ConnectionType::Spotify] connections. + /// + /// # Reference + /// See + pub async fn get_connection_access_token( + &mut self, + connection_type: ConnectionType, + connection_account_id: &String, + ) -> ChorusResult { + let connection_type_string = serde_json::to_string(&connection_type) + .expect("Failed to serialize connection type!") + .replace('"', ""); + + let request = Client::new() + .get(format!( + "{}/users/@me/connections/{}/{}/access-token", + self.belongs_to.read().unwrap().urls.api, + connection_type_string, + connection_account_id + )) + .header("Authorization", self.token()); + + let chorus_request = ChorusRequest { + request, + limit_type: LimitType::default(), + }; + + chorus_request + .deserialize_response::(self) + .await + .map(|res| res.access_token) + } + + /// Fetches a list of [subreddits](crate::types::ConnectionSubreddit) + /// the connected account moderates. + /// + /// Only available for [ConnectionType::Reddit] connections. + /// + /// # Reference + /// See + pub async fn get_connection_subreddits( + &mut self, + connection_account_id: &String, + ) -> ChorusResult> { + let request = Client::new() + .get(format!( + "{}/users/@me/connections/reddit/{}/subreddits", + self.belongs_to.read().unwrap().urls.api, + connection_account_id + )) + .header("Authorization", self.token()); + + let chorus_request = ChorusRequest { + request, + limit_type: LimitType::default(), + }; + + chorus_request.deserialize_response(self).await + } } diff --git a/src/gateway/events.rs b/src/gateway/events.rs index ae1ce85..4663fe1 100644 --- a/src/gateway/events.rs +++ b/src/gateway/events.rs @@ -76,6 +76,7 @@ pub struct Message { #[derive(Default, Debug)] pub struct User { pub update: Publisher, + pub connections_update: Publisher, pub note_update: Publisher, pub guild_settings_update: Publisher, pub presence_update: Publisher, diff --git a/src/gateway/gateway.rs b/src/gateway/gateway.rs index 32180b1..33804b3 100644 --- a/src/gateway/gateway.rs +++ b/src/gateway/gateway.rs @@ -414,6 +414,7 @@ impl Gateway { "STAGE_INSTANCE_DELETE" => stage_instance.delete, "TYPING_START" => user.typing_start, "USER_UPDATE" => user.update, // TODO + "USER_CONNECTIONS_UPDATE" => user.connections_update, // TODO "USER_NOTE_UPDATE" => user.note_update, "USER_GUILD_SETTINGS_UPDATE" => user.guild_settings_update, "VOICE_STATE_UPDATE" => voice.state_update, // TODO diff --git a/src/types/entities/connection.rs b/src/types/entities/connection.rs index b4e6b3d..f3472b7 100644 --- a/src/types/entities/connection.rs +++ b/src/types/entities/connection.rs @@ -1,7 +1,4 @@ -use std::{ - collections::HashMap, - fmt::Display, -}; +use std::{collections::HashMap, fmt::Display}; use serde::{Deserialize, Serialize}; use serde_repr::{Deserialize_repr, Serialize_repr}; @@ -153,6 +150,39 @@ impl Display for ConnectionType { } } +impl ConnectionType { + /// Returns an array of all the connections + pub fn array() -> [ConnectionType; 25] { + [ + ConnectionType::AmazonMusic, + ConnectionType::BattleNet, + ConnectionType::Bungie, + ConnectionType::Contacts, + ConnectionType::Crunchyroll, + ConnectionType::Domain, + ConnectionType::Ebay, + ConnectionType::EpicGames, + ConnectionType::Facebook, + ConnectionType::GitHub, + ConnectionType::Instagram, + ConnectionType::LeagueOfLegends, + ConnectionType::PayPal, + ConnectionType::Playstation, + ConnectionType::Reddit, + ConnectionType::RiotGames, + ConnectionType::Samsung, + ConnectionType::Spotify, + ConnectionType::Skype, + ConnectionType::Steam, + ConnectionType::TikTok, + ConnectionType::Twitch, + ConnectionType::Twitter, + ConnectionType::Xbox, + ConnectionType::YouTube, + ] + } +} + #[derive( Serialize_repr, Deserialize_repr, Debug, Clone, Eq, PartialEq, Hash, Copy, PartialOrd, Ord, )] @@ -189,3 +219,17 @@ impl Display for TwoWayLinkType { f.write_str(format!("{:?}", self).as_str()) } } + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] +/// Defines a subreddit as fetched through a Reddit connection. +/// +/// # Reference +/// See +pub struct ConnectionSubreddit { + /// The subreddit's internal id, e.g. "t5_388p4" + pub id: String, + /// How many reddit users follow the subreddit + pub subscribers: usize, + /// The subreddit's relative url, e.g. "/r/discordapp/" + pub url: String, +} diff --git a/src/types/events/user.rs b/src/types/events/user.rs index acfc298..fc72be4 100644 --- a/src/types/events/user.rs +++ b/src/types/events/user.rs @@ -7,6 +7,7 @@ use serde::{Deserialize, Serialize}; use crate::types::entities::PublicUser; use crate::types::events::WebSocketEvent; use crate::types::utils::Snowflake; +use crate::types::Connection; #[derive(Debug, Default, Deserialize, Serialize, Clone, PartialEq, Eq, WebSocketEvent)] /// See ; @@ -16,6 +17,15 @@ pub struct UserUpdate { pub user: PublicUser, } +#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq, WebSocketEvent)] +/// Sent to indicate updates to a user's [Connection]. +/// +/// Not documented anywhere +pub struct UserConnectionsUpdate { + #[serde(flatten)] + pub connection: Connection, +} + #[derive(Debug, Default, Deserialize, Serialize, Clone, PartialEq, Eq, WebSocketEvent)] /// See ; /// diff --git a/src/types/schema/user.rs b/src/types/schema/user.rs index 93202f2..808b96c 100644 --- a/src/types/schema/user.rs +++ b/src/types/schema/user.rs @@ -297,3 +297,80 @@ pub struct AuthorizeConnectionSchema { pub(crate) struct AuthorizeConnectionReturn { pub url: String, } + +#[derive(Debug, Default, Deserialize, Serialize, Clone, PartialEq, Eq)] +/// Json schema for the route POST /connections/{connection.type}/callback ([crate::instance::ChorusUser::create_connection_callback]). +/// +/// See +pub struct CreateConnectionCallbackSchema { + /// The authorization code for the connection + pub code: String, + /// The "state" used to authorize a connection + // TODO: what is this? + pub state: String, + pub two_way_link_code: Option, + pub insecure: Option, + pub friend_sync: Option, + /// Additional parameters used for OpenID Connect + // FIXME: Is this correct? in other connections additional info + // is provided like this, only being string - string + pub openid_params: Option> +} + +#[derive(Debug, Default, Deserialize, Serialize, Clone, PartialEq, Eq)] +/// Json schema for the route PUT /users/@me/connections/contacts/{connection.id} ([crate::instance::ChorusUser::create_contact_sync_connection]). +/// +/// See +pub struct CreateContactSyncConnectionSchema { + /// The username of the connection account + pub name: String, + /// Whether to sync friends over the connection + pub friend_sync: Option, +} + +#[derive(Debug, Default, Deserialize, Serialize, Clone, PartialEq, Eq)] +/// Json schema for the route PATCH /users/@me/connections/{connection.type}/{connection.id} ([crate::instance::ChorusUser::modify_connection]). +/// +/// Note: not all connection types support all parameters. +/// +/// See +pub struct ModifyConnectionSchema { + /// The connection account's username + /// + /// Note: We have not found which connection this could apply to + #[serde(skip_serializing_if = "Option::is_none")] + pub name: Option, + + /// Whether activities related to this connection will be shown in presence + /// + /// e.g. on a Spotify connection, "Display Spotify as your status" + #[serde(skip_serializing_if = "Option::is_none")] + pub show_activity: Option, + + /// Whether or not to sync friends from this connection + /// + /// Note: we have not found which connections this can apply to + #[serde(skip_serializing_if = "Option::is_none")] + pub friend_sync: Option, + + /// Whether to show additional metadata on the user's profile + /// + /// e.g. on a Steam connection, "Display details on profile" + /// (number of games, items, member since) + /// + /// on a Twitter connection, number of posts / followers, member since + #[serde(skip_serializing_if = "Option::is_none")] + pub metadata_visibility: Option, + + /// Whether to show the connection on the user's profile + #[serde(skip_serializing_if = "Option::is_none")] + pub visibility: Option, +} + +#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)] +/// Internal type for the [crate::instance::ChorusUser::get_connection_access_token] endpoint. +/// +/// See +pub(crate) struct GetConnectionAccessTokenReturn { + pub access_token: String, +}