From 87aad461fd4a08e02800a46ab6367819a3df02b1 Mon Sep 17 00:00:00 2001 From: kozabrada123 Date: Fri, 9 Aug 2024 10:24:38 +0200 Subject: [PATCH] feat: add get_user_harvest & create_user_harvest Also adds /types/entities/harvest.rs, types for Harvest --- src/api/users/users.rs | 103 ++++++++++++++++++++++++++++++++-- src/types/entities/harvest.rs | 96 +++++++++++++++++++++++++++++++ src/types/entities/mod.rs | 2 + src/types/schema/user.rs | 58 +++++++++++-------- 4 files changed, 231 insertions(+), 28 deletions(-) create mode 100644 src/types/entities/harvest.rs diff --git a/src/api/users/users.rs b/src/api/users/users.rs index 6596782..92fbb6e 100644 --- a/src/api/users/users.rs +++ b/src/api/users/users.rs @@ -12,9 +12,10 @@ use crate::{ instance::{ChorusUser, Instance}, ratelimiter::ChorusRequest, types::{ - DeleteDisableUserSchema, GetPomeloEligibilityReturn, GetPomeloSuggestionsReturn, - GetRecentMentionsSchema, GetUserProfileSchema, LimitType, PublicUser, Snowflake, User, - UserModifyProfileSchema, UserModifySchema, UserProfile, UserProfileMetadata, UserSettings, + CreateUserHarvestSchema, DeleteDisableUserSchema, GetPomeloEligibilityReturn, + GetPomeloSuggestionsReturn, GetRecentMentionsSchema, GetUserProfileSchema, Harvest, + HarvestBackendType, LimitType, PublicUser, Snowflake, User, UserModifyProfileSchema, + UserModifySchema, UserProfile, UserProfileMetadata, UserSettings, VerifyUserEmailChangeResponse, VerifyUserEmailChangeSchema, }, }; @@ -392,7 +393,7 @@ impl ChorusUser { /// Fires a RecentMentionDelete gateway event. (Note: yet to be implemented in chorus, see [#545](https://github.com/polyphony-chat/chorus/issues/545)) /// /// As of 2024/08/09, Spacebar does not yet implement this endpoint. - /// + /// /// See pub async fn delete_recent_mention(&mut self, message_id: Snowflake) -> ChorusResult<()> { let request = Client::new() @@ -410,6 +411,100 @@ impl ChorusUser { chorus_request.handle_request_as_result(self).await } + + /// If it exists, returns the most recent [Harvest] (personal data harvest request). + /// + /// To create a new [Harvest], see [Self::create_harvest]. + /// + /// As of 2024/08/09, Spacebar does not yet implement this endpoint. (Or data harvesting) + /// + /// See + pub async fn get_harvest(&mut self) -> ChorusResult> { + let request = Client::new() + .get(format!( + "{}/users/@me/harvest", + self.belongs_to.read().unwrap().urls.api, + )) + .header("Authorization", self.token()); + + let chorus_request = ChorusRequest { + request, + limit_type: LimitType::default(), + }; + + // Manual handling, because a 204 with no harvest is a success state + // TODO: Maybe make this a method on ChorusRequest if we need it a lot + let response = chorus_request.send_request(self).await?; + log::trace!("Got response: {:?}", response); + + if response.status() == http::StatusCode::NO_CONTENT { + return Ok(None); + } + + let response_text = match response.text().await { + Ok(string) => string, + Err(e) => { + return Err(ChorusError::InvalidResponse { + error: format!( + "Error while trying to process the HTTP response into a String: {}", + e + ), + }); + } + }; + + let object = match serde_json::from_str::(&response_text) { + Ok(object) => object, + Err(e) => { + return Err(ChorusError::InvalidResponse { + error: format!( + "Error while trying to deserialize the JSON response into requested type T: {}. JSON Response: {}", + e, response_text + ), + }) + } + }; + Ok(Some(object)) + } + + /// Creates a personal data harvest request ([Harvest]) for the current user. + /// + /// To fetch the latest existing harvest, see [Self::get_harvest]. + /// + /// Invalid options in the backends array are ignored. + /// + /// If the array is empty (after ignoring), it requests all [HarvestBackendType]s. + /// + /// As of 2024/08/09, Spacebar does not yet implement this endpoint. (Or data harvesting) + /// + /// See + pub async fn create_harvest( + &mut self, + backends: Vec, + ) -> ChorusResult { + let schema = if backends.is_empty() { + CreateUserHarvestSchema { backends: None } + } else { + CreateUserHarvestSchema { + backends: Some(backends), + } + }; + + let request = Client::new() + .post(format!( + "{}/users/@me/harvest", + self.belongs_to.read().unwrap().urls.api, + )) + .header("Authorization", self.token()) + .json(&schema); + + let chorus_request = ChorusRequest { + request, + limit_type: LimitType::default(), + }; + + chorus_request.deserialize_response(self).await + } } impl User { diff --git a/src/types/entities/harvest.rs b/src/types/entities/harvest.rs new file mode 100644 index 0000000..d9f86ac --- /dev/null +++ b/src/types/entities/harvest.rs @@ -0,0 +1,96 @@ +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; +use serde_repr::{Deserialize_repr, Serialize_repr}; + +use crate::types::Snowflake; + +#[cfg(feature = "client")] +use crate::gateway::Updateable; + +// FIXME: Should this type be Composite? +#[derive(Serialize, Deserialize, Debug, Default, Clone, Copy, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "sqlx", derive(sqlx::FromRow))] +/// A user's data harvest. +/// +/// # Reference +/// +/// See +pub struct Harvest { + pub harvest_id: Snowflake, + /// The id of the user being harvested + pub user_id: Snowflake, + pub status: HarvestStatus, + /// The time the harvest was created + pub created_at: DateTime, + /// The time the harvest was last polled + pub polled_at: Option>, + /// The time the harvest was completed + pub completed_at: Option>, +} + +#[cfg(feature = "client")] +impl Updateable for Harvest { + #[cfg(not(tarpaulin_include))] + fn id(&self) -> Snowflake { + self.harvest_id + } +} + +#[derive( + Serialize_repr, + Deserialize_repr, + Debug, + Default, + Clone, + Eq, + PartialEq, + Hash, + Copy, + PartialOrd, + Ord, +)] +#[cfg_attr(feature = "sqlx", derive(sqlx::Type))] +#[repr(u8)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +/// Current status of a [Harvest] +/// +/// See and +pub enum HarvestStatus { + /// The harvest is queued and has not been started + Queued = 0, + /// The harvest is currently running / being processed + Running = 1, + /// The harvest has failed + Failed = 2, + /// The harvest has been completed successfully + Completed = 3, + #[default] + Unknown = 4, +} + +#[derive(Serialize, Deserialize, Debug, Clone, Eq, PartialEq, Hash, Copy, PartialOrd, Ord)] +#[cfg_attr(feature = "sqlx", derive(sqlx::Type))] +/// A type of backend / service a harvest can be requested for. +/// +/// See and +pub enum HarvestBackendType { + /// All account information; + Accounts, + /// Actions the user has taken; + /// + /// Represented as "Your Activity" in the discord client + Analytics, + /// First-party embedded activity information; + /// + /// e.g.: Chess in the Park, Checkers in the Park, Poker Night 2.0; + /// Sketch Heads, Watch Together, Letter League, Land-io, Know What I Meme + Activities, + /// The user's messages + Messages, + /// Official Discord programes; + /// + /// e.g.: Partner, HypeSquad, Verified Server + Programs, + /// Guilds the user is a member of; + Servers, +} diff --git a/src/types/entities/mod.rs b/src/types/entities/mod.rs index 2fec4ca..c736b99 100644 --- a/src/types/entities/mod.rs +++ b/src/types/entities/mod.rs @@ -11,6 +11,7 @@ pub use config::*; pub use emoji::*; pub use guild::*; pub use guild_member::*; +pub use harvest::*; pub use integration::*; pub use invite::*; pub use message::*; @@ -52,6 +53,7 @@ mod config; mod emoji; mod guild; mod guild_member; +mod harvest; mod integration; mod invite; mod message; diff --git a/src/types/schema/user.rs b/src/types/schema/user.rs index f7a97d2..bc641d6 100644 --- a/src/types/schema/user.rs +++ b/src/types/schema/user.rs @@ -7,7 +7,7 @@ use std::collections::HashMap; use chrono::NaiveDate; use serde::{Deserialize, Serialize}; -use crate::types::{Snowflake, ThemeColors}; +use crate::types::{HarvestBackendType, Snowflake, ThemeColors}; #[derive(Debug, Default, Deserialize, Serialize, Clone, PartialEq, Eq)] #[serde(rename_all = "snake_case")] @@ -218,7 +218,7 @@ pub struct GetUserProfileSchema { /// /// See pub(crate) struct GetPomeloSuggestionsReturn { - pub username: String + pub username: String, } #[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)] @@ -226,7 +226,7 @@ pub(crate) struct GetPomeloSuggestionsReturn { /// /// See pub(crate) struct GetPomeloEligibilityReturn { - pub taken: bool + pub taken: bool, } #[derive(Debug, Default, Deserialize, Serialize, Clone, Copy, PartialEq, Eq)] @@ -235,25 +235,35 @@ pub(crate) struct GetPomeloEligibilityReturn { /// /// See pub struct GetRecentMentionsSchema { - /// Only fetch messages before this message id - /// - /// Due to the nature of snowflakes, this can be easily used to fetch - /// messages before a certain timestamp - pub before: Option, - /// Max number of messages to return - /// - /// Should be between 1 and 100. - /// - /// If unset the limit is 25 messages - pub limit: Option, - /// Limit messages to a specific guild - pub guild_id: Option, - /// Whether to include role mentions. - /// - /// If unset the server assumes true - pub roles: Option, - /// Whether to include @everyone and @here mentions. - /// - /// If unset the server assumes true - pub everyone: Option, + /// Only fetch messages before this message id + /// + /// Due to the nature of snowflakes, this can be easily used to fetch + /// messages before a certain timestamp + pub before: Option, + /// Max number of messages to return + /// + /// Should be between 1 and 100. + /// + /// If unset the limit is 25 messages + pub limit: Option, + /// Limit messages to a specific guild + pub guild_id: Option, + /// Whether to include role mentions. + /// + /// If unset the server assumes true + pub roles: Option, + /// Whether to include @everyone and @here mentions. + /// + /// If unset the server assumes true + pub everyone: Option, +} + +#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)] +/// Internal type for the [crate::instance::ChorusUser::create_harvest] endpoint. +// (koza): imo it's nicer if the user can just pass a vec, instead of having to bother with +// a specific type +/// +/// See +pub(crate) struct CreateUserHarvestSchema { + pub backends: Option>, }