Compare commits

...

3 Commits

Author SHA1 Message Date
kozabrada123 e06bc147b4 feat: add affinities 2024-08-13 09:21:59 +02:00
kozabrada123 f966a787d9 feat: add connected_accounts to UserProfile 2024-08-13 08:48:13 +02:00
kozabrada123 a1caa38bfd 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
2024-08-13 08:43:37 +02:00
8 changed files with 491 additions and 13 deletions

View File

@ -1,15 +1,26 @@
use futures_util::FutureExt;
use reqwest::Client; use reqwest::Client;
use crate::{ use crate::{
errors::ChorusResult, errors::ChorusResult,
instance::ChorusUser, instance::ChorusUser,
ratelimiter::ChorusRequest, ratelimiter::ChorusRequest,
types::{AuthorizeConnectionReturn, AuthorizeConnectionSchema, ConnectionType, LimitType}, types::{
AuthorizeConnectionReturn, AuthorizeConnectionSchema, Connection, ConnectionSubreddit,
ConnectionType, CreateConnectionCallbackSchema, CreateContactSyncConnectionSchema,
GetConnectionAccessTokenReturn, LimitType, ModifyConnectionSchema,
},
}; };
impl ChorusUser { impl ChorusUser {
/// Fetches a url that can be used for authorizing a new connection. /// 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 /// # Reference
/// See <https://docs.discord.sex/resources/user#authorize-user-connection> /// See <https://docs.discord.sex/resources/user#authorize-user-connection>
/// ///
@ -44,4 +55,249 @@ impl ChorusUser {
.await .await
.map(|response| response.url) .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 <https://docs.discord.sex/resources/user#create-user-connection-callback>
// 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<Connection> {
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 <https://docs.discord.sex/resources/user#create-contact-sync-connection>
pub async fn create_contact_sync_connection(
&mut self,
connection_account_id: &String,
json_schema: CreateContactSyncConnectionSchema,
) -> ChorusResult<Connection> {
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 (<https://docs.discord.sex/resources/user#create-domain-connection>)
// It requires changing how chorus handles errors to support properly
/// Fetches the current user's [Connection]s
///
/// # Reference
/// See <https://docs.discord.sex/resources/user#get-user-connections>
pub async fn get_connections(&mut self) -> ChorusResult<Vec<Connection>> {
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 <https://docs.discord.sex/resources/user#refresh-user-connection>
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 <https://docs.discord.sex/resources/user#modify-user-connection>
pub async fn modify_connection(
&mut self,
connection_type: ConnectionType,
connection_account_id: &String,
json_schema: ModifyConnectionSchema,
) -> ChorusResult<Connection> {
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 <https://docs.discord.sex/resources/user#delete-user-connection>
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 <https://docs.discord.sex/resources/user#get-user-connection-access-token>
pub async fn get_connection_access_token(
&mut self,
connection_type: ConnectionType,
connection_account_id: &String,
) -> ChorusResult<String> {
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::<GetConnectionAccessTokenReturn>(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 <https://docs.discord.sex/resources/user#get-user-connection-subreddits>
pub async fn get_connection_subreddits(
&mut self,
connection_account_id: &String,
) -> ChorusResult<Vec<ConnectionSubreddit>> {
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
}
} }

View File

@ -17,10 +17,11 @@ use crate::{
types::{ types::{
AuthorizeConnectionSchema, ConnectionType, CreateUserHarvestSchema, AuthorizeConnectionSchema, ConnectionType, CreateUserHarvestSchema,
DeleteDisableUserSchema, GetPomeloEligibilityReturn, GetPomeloSuggestionsReturn, DeleteDisableUserSchema, GetPomeloEligibilityReturn, GetPomeloSuggestionsReturn,
GetRecentMentionsSchema, GetUserProfileSchema, Harvest, HarvestBackendType, LimitType, GetRecentMentionsSchema, GetUserProfileSchema, GuildAffinities, Harvest,
ModifyUserNoteSchema, PublicUser, Snowflake, User, UserModifyProfileSchema, HarvestBackendType, LimitType, ModifyUserNoteSchema, PublicUser, Snowflake, User,
UserModifySchema, UserNote, UserProfile, UserProfileMetadata, UserSettings, UserAffinities, UserModifyProfileSchema, UserModifySchema, UserNote, UserProfile,
VerifyUserEmailChangeResponse, VerifyUserEmailChangeSchema, UserProfileMetadata, UserSettings, VerifyUserEmailChangeResponse,
VerifyUserEmailChangeSchema,
}, },
}; };
@ -571,6 +572,48 @@ impl ChorusUser {
) -> ChorusResult<()> { ) -> ChorusResult<()> {
User::set_note(self, target_user_id, note).await User::set_note(self, target_user_id, note).await
} }
/// Fetches the current user's affinity scores for other users.
///
/// (Affinity scores are a measure of how likely a user is to be friends with another user.)
///
/// # Reference
/// See <https://docs.discord.sex/resources/user#get-user-affinities>
pub async fn get_user_affinities(&mut self) -> ChorusResult<UserAffinities> {
let request = Client::new()
.get(format!(
"{}/users/@me/affinities/users",
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
}
/// Fetches the current user's affinity scores for their joined guilds.
///
/// # Reference
/// See <https://docs.discord.sex/resources/user#get-guild-affinities>
pub async fn get_guild_affinities(&mut self) -> ChorusResult<GuildAffinities> {
let request = Client::new()
.get(format!(
"{}/users/@me/affinities/guilds",
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
}
} }
impl User { impl User {

View File

@ -76,6 +76,7 @@ pub struct Message {
#[derive(Default, Debug)] #[derive(Default, Debug)]
pub struct User { pub struct User {
pub update: Publisher<types::UserUpdate>, pub update: Publisher<types::UserUpdate>,
pub connections_update: Publisher<types::UserConnectionsUpdate>,
pub note_update: Publisher<types::UserNoteUpdate>, pub note_update: Publisher<types::UserNoteUpdate>,
pub guild_settings_update: Publisher<types::UserGuildSettingsUpdate>, pub guild_settings_update: Publisher<types::UserGuildSettingsUpdate>,
pub presence_update: Publisher<types::PresenceUpdate>, pub presence_update: Publisher<types::PresenceUpdate>,

View File

@ -414,6 +414,7 @@ impl Gateway {
"STAGE_INSTANCE_DELETE" => stage_instance.delete, "STAGE_INSTANCE_DELETE" => stage_instance.delete,
"TYPING_START" => user.typing_start, "TYPING_START" => user.typing_start,
"USER_UPDATE" => user.update, // TODO "USER_UPDATE" => user.update, // TODO
"USER_CONNECTIONS_UPDATE" => user.connections_update, // TODO
"USER_NOTE_UPDATE" => user.note_update, "USER_NOTE_UPDATE" => user.note_update,
"USER_GUILD_SETTINGS_UPDATE" => user.guild_settings_update, "USER_GUILD_SETTINGS_UPDATE" => user.guild_settings_update,
"VOICE_STATE_UPDATE" => voice.state_update, // TODO "VOICE_STATE_UPDATE" => voice.state_update, // TODO

View File

@ -1,7 +1,4 @@
use std::{ use std::{collections::HashMap, fmt::Display};
collections::HashMap,
fmt::Display,
};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use serde_repr::{Deserialize_repr, Serialize_repr}; 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( #[derive(
Serialize_repr, Deserialize_repr, Debug, Clone, Eq, PartialEq, Hash, Copy, PartialOrd, Ord, 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()) 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 <https://docs.discord.sex/resources/user#subreddit-structure>
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,
}

View File

@ -23,7 +23,7 @@ use crate::gateway::GatewayHandle;
#[cfg(feature = "client")] #[cfg(feature = "client")]
use chorus_macros::{Composite, Updateable}; use chorus_macros::{Composite, Updateable};
use super::{Emoji, GuildMember}; use super::{Emoji, GuildMember, PublicConnection};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)] #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
#[cfg_attr(feature = "sqlx", derive(sqlx::Type))] #[cfg_attr(feature = "sqlx", derive(sqlx::Type))]
@ -304,8 +304,9 @@ pub struct UserProfile {
pub mutual_friends_count: Option<u32>, pub mutual_friends_count: Option<u32>,
// TODO: Add connections! pub connected_accounts: Vec<PublicConnection>,
// TODO: And application role connections!
// TODO: Add application role connections!
/// The type of premium (Nitro) a user has /// The type of premium (Nitro) a user has
pub premium_type: Option<PremiumType>, pub premium_type: Option<PremiumType>,
/// The date the user's premium (Nitro) subscribtion started /// The date the user's premium (Nitro) subscribtion started
@ -784,3 +785,27 @@ pub struct UserNote {
/// The ID of the user who created the note (always the current user) /// The ID of the user who created the note (always the current user)
pub user_id: Snowflake, pub user_id: Snowflake,
} }
/// Structure which defines an affinity the local user has with another user.
///
/// # Reference
/// See <https://docs.discord.sex/resources/user#user-affinity-structure>
#[derive(Debug, Deserialize, Serialize, Clone, Copy, PartialEq, PartialOrd)]
pub struct UserAffinity {
/// The other user's id
pub user_id: Snowflake,
/// The affinity score
pub affinity: f32,
}
/// Structure which defines an affinity the local user has with a guild.
///
/// # Reference
/// See <https://docs.discord.sex/resources/user#guild-affinity-structure>
#[derive(Debug, Deserialize, Serialize, Clone, Copy, PartialEq, PartialOrd)]
pub struct GuildAffinity {
/// The guild's id
pub guild_id: Snowflake,
/// The affinity score
pub affinity: f32,
}

View File

@ -7,6 +7,7 @@ use serde::{Deserialize, Serialize};
use crate::types::entities::PublicUser; use crate::types::entities::PublicUser;
use crate::types::events::WebSocketEvent; use crate::types::events::WebSocketEvent;
use crate::types::utils::Snowflake; use crate::types::utils::Snowflake;
use crate::types::Connection;
#[derive(Debug, Default, Deserialize, Serialize, Clone, PartialEq, Eq, WebSocketEvent)] #[derive(Debug, Default, Deserialize, Serialize, Clone, PartialEq, Eq, WebSocketEvent)]
/// See <https://discord.com/developers/docs/topics/gateway-events#user-update>; /// See <https://discord.com/developers/docs/topics/gateway-events#user-update>;
@ -16,6 +17,15 @@ pub struct UserUpdate {
pub user: PublicUser, 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)] #[derive(Debug, Default, Deserialize, Serialize, Clone, PartialEq, Eq, WebSocketEvent)]
/// See <https://docs.discord.sex/topics/gateway-events#user-note-update-structure>; /// See <https://docs.discord.sex/topics/gateway-events#user-note-update-structure>;
/// ///

View File

@ -7,7 +7,9 @@ use std::collections::HashMap;
use chrono::NaiveDate; use chrono::NaiveDate;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::types::{HarvestBackendType, Snowflake, ThemeColors, TwoWayLinkType}; use crate::types::{
GuildAffinity, HarvestBackendType, Snowflake, ThemeColors, TwoWayLinkType, UserAffinity,
};
#[derive(Debug, Default, Deserialize, Serialize, Clone, PartialEq, Eq)] #[derive(Debug, Default, Deserialize, Serialize, Clone, PartialEq, Eq)]
#[serde(rename_all = "snake_case")] #[serde(rename_all = "snake_case")]
@ -297,3 +299,99 @@ pub struct AuthorizeConnectionSchema {
pub(crate) struct AuthorizeConnectionReturn { pub(crate) struct AuthorizeConnectionReturn {
pub url: String, 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 <https://docs.discord.sex/resources/user#create-user-connection-callback>
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<String>,
pub insecure: Option<bool>,
pub friend_sync: Option<bool>,
/// 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<HashMap<String, String>>,
}
#[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 <https://docs.discord.sex/resources/user#create-contact-sync-connection>
pub struct CreateContactSyncConnectionSchema {
/// The username of the connection account
pub name: String,
/// Whether to sync friends over the connection
pub friend_sync: Option<bool>,
}
#[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 <https://docs.discord.sex/resources/user#modify-user-connection>
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<String>,
/// 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<bool>,
/// 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<bool>,
/// 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<bool>,
/// Whether to show the connection on the user's profile
#[serde(skip_serializing_if = "Option::is_none")]
pub visibility: Option<bool>,
}
#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)]
/// Internal type for the [crate::instance::ChorusUser::get_connection_access_token] endpoint.
///
/// See <https://docs.discord.sex/resources/user#get-user-connection-access-token>
pub(crate) struct GetConnectionAccessTokenReturn {
pub access_token: String,
}
#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
/// Return type for the [crate::instance::ChorusUser::get_user_affinities] endpoint.
///
/// See <https://docs.discord.sex/resources/user#get-user-affinities>
pub struct UserAffinities {
pub user_affinities: Vec<UserAffinity>,
// FIXME: Is this also a UserAffinity vec?
// Also, no idea what this means
pub inverse_user_affinities: Vec<UserAffinity>,
}
#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
/// Return type for the [crate::instance::ChorusUser::get_guild_affinities] endpoint.
///
/// See <https://docs.discord.sex/resources/user#get-guild-affinities>
pub struct GuildAffinities {
pub guild_affinities: Vec<GuildAffinity>,
}