diff --git a/src/models/ctx/ctx.rs b/src/models/ctx/ctx.rs index b30d0402b..3f7b136ff 100644 --- a/src/models/ctx/ctx.rs +++ b/src/models/ctx/ctx.rs @@ -1,10 +1,10 @@ -use crate::constants::LIBRARY_COLLECTION_NAME; +use crate::constants::{LIBRARY_COLLECTION_NAME, OFFICIAL_ADDONS}; use crate::models::common::{DescriptorLoadable, Loadable, ResourceLoadable}; use crate::models::ctx::{ update_events, update_library, update_notifications, update_profile, update_search_history, update_streams, update_trakt_addon, CtxError, }; -use crate::runtime::msg::{Action, ActionCtx, Event, Internal, Msg}; +use crate::runtime::msg::{Action, ActionCtx, CtxAuthResponse, Event, Internal, Msg}; use crate::runtime::{Effect, EffectFuture, Effects, Env, EnvFutureExt, Update}; use crate::types::api::{ fetch_api, APIRequest, APIResult, AuthRequest, AuthResponse, CollectionResponse, @@ -103,7 +103,7 @@ impl Update for Ctx { let profile_effects = update_profile::(&mut self.profile, &mut self.streams, &self.status, msg); let library_effects = - update_library::(&mut self.library, &self.profile, &self.status, msg); + update_library::(&mut self.library, &mut self.profile, &self.status, msg); let streams_effects = update_streams::(&mut self.streams, &self.status, msg); let search_history_effects = update_search_history::(&mut self.search_history, &self.status, msg); @@ -139,7 +139,7 @@ impl Update for Ctx { let profile_effects = update_profile::(&mut self.profile, &mut self.streams, &self.status, msg); let library_effects = - update_library::(&mut self.library, &self.profile, &self.status, msg); + update_library::(&mut self.library, &mut self.profile, &self.status, msg); let trakt_addon_effects = update_trakt_addon::( &mut self.trakt_addon, &self.profile, @@ -193,7 +193,7 @@ impl Update for Ctx { let profile_effects = update_profile::(&mut self.profile, &mut self.streams, &self.status, msg); let library_effects = - update_library::(&mut self.library, &self.profile, &self.status, msg); + update_library::(&mut self.library, &mut self.profile, &self.status, msg); let streams_effects = update_streams::(&mut self.streams, &self.status, msg); let trakt_addon_effects = update_trakt_addon::( &mut self.trakt_addon, @@ -229,66 +229,80 @@ fn authenticate(auth_request: &AuthRequest) -> Effect { let auth_api = APIRequest::Auth(auth_request.clone()); EffectFuture::Concurrent( - E::flush_analytics() - .then(move |_| { - fetch_api::(&auth_api) - .inspect(move |result| trace!(?result, ?auth_api, "Auth request")) - }) - .map_err(CtxError::from) - .and_then(|result| match result { - APIResult::Ok { result } => future::ok(result), - APIResult::Err { error } => future::err(CtxError::from(error)), - }) - .map_ok(|AuthResponse { key, user }| Auth { key, user }) - .and_then(|auth| { - let addon_collection_fut = { - let request = APIRequest::AddonCollectionGet { - auth_key: auth.key.to_owned(), - update: true, - }; - fetch_api::(&request) - .inspect(move |result| { - trace!(?result, ?request, "Get user's Addon Collection request") - }) - .map_err(CtxError::from) - .and_then(|result| match result { - APIResult::Ok { result } => future::ok(result), - APIResult::Err { error } => future::err(CtxError::from(error)), - }) - .map_ok(|CollectionResponse { addons, .. }| addons) - }; + async { + E::flush_analytics().await; - let datastore_library_fut = { - let request = DatastoreRequest { - auth_key: auth.key.to_owned(), - collection: LIBRARY_COLLECTION_NAME.to_owned(), - command: DatastoreCommand::Get { - ids: vec![], - all: true, - }, - }; + // return an error only if the auth request fails + let auth = fetch_api::(&auth_api) + .inspect(move |result| trace!(?result, ?auth_api, "Auth request")) + .await + .map_err(CtxError::from) + .and_then(|result| match result { + APIResult::Ok { result } => Ok(result), + APIResult::Err { error } => Err(CtxError::from(error)), + }) + .map(|AuthResponse { key, user }| Auth { key, user })?; - fetch_api::(&request) - .inspect(move |result| { - trace!(?result, ?request, "Get user's Addon Collection request") - }) - .map_err(CtxError::from) - .and_then(|result| match result { - APIResult::Ok { result } => future::ok(result.0), - APIResult::Err { error } => future::err(CtxError::from(error)), - }) + let addon_collection_fut = { + let request = APIRequest::AddonCollectionGet { + auth_key: auth.key.to_owned(), + update: true, }; + fetch_api::(&request) + .inspect(move |result| { + trace!(?result, ?request, "Get user's Addon Collection request") + }) + .map_err(CtxError::from) + .and_then(|result| match result { + APIResult::Ok { result } => future::ok(result), + APIResult::Err { error } => future::err(CtxError::from(error)), + }) + .map_ok(|CollectionResponse { addons, .. }| addons) + }; - future::try_join(addon_collection_fut, datastore_library_fut) - .map_ok(move |(addons, library_items)| (auth, addons, library_items)) + let datastore_library_fut = { + let request = DatastoreRequest { + auth_key: auth.key.to_owned(), + collection: LIBRARY_COLLECTION_NAME.to_owned(), + command: DatastoreCommand::Get { + ids: vec![], + all: true, + }, + }; + + fetch_api::(&request) + .inspect(move |result| { + trace!(?result, ?request, "Get user's Addon Collection request") + }) + .map_err(CtxError::from) + .and_then(|result| match result { + APIResult::Ok { result } => future::ok(result.0), + APIResult::Err { error } => future::err(CtxError::from(error)), + }) + }; + + let (addon_collection_result, datastore_library_result) = + future::join(addon_collection_fut, datastore_library_fut).await; + + // lock if the result from fetching the addons has failed + let addons_locked = addon_collection_result.is_err(); + // set the flag to true if fetching of the library has failed + let library_missing = datastore_library_result.is_err(); + Ok(CtxAuthResponse { + auth, + addons: addon_collection_result.unwrap_or(OFFICIAL_ADDONS.clone()), + addons_locked, + library_items: datastore_library_result.unwrap_or_default(), + library_missing, }) - .map(enclose!((auth_request) move |result| { - let internal_msg = Msg::Internal(Internal::CtxAuthResult(auth_request, result)); + } + .map(enclose!((auth_request) move |result| { + let internal_msg = Msg::Internal(Internal::CtxAuthResult(auth_request, result)); - event!(Level::TRACE, internal_message = ?internal_msg); - internal_msg - })) - .boxed_env(), + event!(Level::TRACE, internal_message = ?internal_msg); + internal_msg + })) + .boxed_env(), ) .into() } diff --git a/src/models/ctx/error.rs b/src/models/ctx/error.rs index 175c825bb..57b3cba75 100644 --- a/src/models/ctx/error.rs +++ b/src/models/ctx/error.rs @@ -38,6 +38,7 @@ pub enum OtherError { AddonNotInstalled, AddonIsProtected, AddonConfigurationRequired, + UserAddonsAreLocked, } impl OtherError { @@ -49,6 +50,7 @@ impl OtherError { OtherError::AddonNotInstalled => "Addon is not installed".to_owned(), OtherError::AddonIsProtected => "Addon is protected".to_owned(), OtherError::AddonConfigurationRequired => "Addon requires configuration".to_owned(), + OtherError::UserAddonsAreLocked => "Syncing Addon from the API failed and we have defaulted the addons to the officials ones until the request succeeds".to_owned(), } } pub fn code(&self) -> u64 { @@ -59,6 +61,7 @@ impl OtherError { OtherError::AddonNotInstalled => 4, OtherError::AddonIsProtected => 5, OtherError::AddonConfigurationRequired => 6, + OtherError::UserAddonsAreLocked => 7, } } } diff --git a/src/models/ctx/update_library.rs b/src/models/ctx/update_library.rs index 7de922d5c..3056db271 100644 --- a/src/models/ctx/update_library.rs +++ b/src/models/ctx/update_library.rs @@ -12,7 +12,7 @@ use crate::{ }, models::ctx::{CtxError, CtxStatus, OtherError}, runtime::{ - msg::{Action, ActionCtx, Event, Internal, Msg}, + msg::{Action, ActionCtx, CtxAuthResponse, Event, Internal, Msg}, Effect, EffectFuture, Effects, Env, EnvFutureExt, }, types::{ @@ -27,7 +27,7 @@ use crate::{ pub fn update_library( library: &mut LibraryBucket, - profile: &Profile, + profile: &mut Profile, status: &CtxStatus, msg: &Msg, ) -> Effects { @@ -175,9 +175,14 @@ pub fn update_library( Effects::one(push_library_to_storage::(library)).unchanged() } Msg::Internal(Internal::CtxAuthResult(auth_request, result)) => match (status, result) { - (CtxStatus::Loading(loading_auth_request), Ok((auth, _, library_items))) - if loading_auth_request == auth_request => - { + ( + CtxStatus::Loading(loading_auth_request), + Ok(CtxAuthResponse { + auth, + library_items, + .. + }), + ) if loading_auth_request == auth_request => { let next_library = LibraryBucket::new(Some(auth.user.id.to_owned()), library_items.to_owned()); if *library != next_library { @@ -243,14 +248,19 @@ pub fn update_library( }, result, )) if Some(loading_auth_key) == auth_key => match result { - Ok(items) => Effects::msg(Msg::Event(Event::LibraryItemsPulledFromAPI { - ids: ids.to_owned(), - })) - .join(Effects::one(update_and_push_items_to_storage::( - library, - items.to_owned(), - ))) - .join(Effects::msg(Msg::Internal(Internal::LibraryChanged(true)))), + Ok(items) => { + // override the missing library flag to indicate that we've successfully updated the local library + profile.library_missing = false; + + Effects::msg(Msg::Event(Event::LibraryItemsPulledFromAPI { + ids: ids.to_owned(), + })) + .join(Effects::one(update_and_push_items_to_storage::( + library, + items.to_owned(), + ))) + .join(Effects::msg(Msg::Internal(Internal::LibraryChanged(true)))) + } Err(error) => Effects::msg(Msg::Event(Event::Error { error: error.to_owned(), source: Box::new(Event::LibraryItemsPulledFromAPI { diff --git a/src/models/ctx/update_profile.rs b/src/models/ctx/update_profile.rs index e04c73c13..246b78748 100644 --- a/src/models/ctx/update_profile.rs +++ b/src/models/ctx/update_profile.rs @@ -1,6 +1,6 @@ use crate::constants::{OFFICIAL_ADDONS, PROFILE_STORAGE_KEY}; use crate::models::ctx::{CtxError, CtxStatus, OtherError}; -use crate::runtime::msg::{Action, ActionCtx, Event, Internal, Msg}; +use crate::runtime::msg::{Action, ActionCtx, CtxAuthResponse, Event, Internal, Msg}; use crate::runtime::{Effect, EffectFuture, Effects, Env, EnvFutureExt}; use crate::types::addon::Descriptor; use crate::types::api::{ @@ -153,6 +153,10 @@ pub fn update_profile( .join(Effects::msg(Msg::Internal(Internal::ProfileChanged))) } Msg::Internal(Internal::UninstallAddon(addon)) => { + if profile.addons_locked { + return addon_install_error_effects(addon, OtherError::UserAddonsAreLocked); + } + let addon_position = profile .addons .iter() @@ -229,6 +233,10 @@ pub fn update_profile( Effects::one(push_profile_to_storage::(profile)).unchanged() } Msg::Internal(Internal::InstallAddon(addon)) => { + if profile.addons_locked { + return addon_install_error_effects(addon, OtherError::UserAddonsAreLocked); + } + if !profile.addons.contains(addon) { if !addon.manifest.behavior_hints.configuration_required { let addon_position = profile @@ -263,12 +271,21 @@ pub fn update_profile( } } Msg::Internal(Internal::CtxAuthResult(auth_request, result)) => match (status, result) { - (CtxStatus::Loading(loading_auth_request), Ok((auth, addons, _))) - if loading_auth_request == auth_request => - { + ( + CtxStatus::Loading(loading_auth_request), + Ok(CtxAuthResponse { + auth, + addons, + addons_locked, + library_missing, + .. + }), + ) if loading_auth_request == auth_request => { let next_profile = Profile { auth: Some(auth.to_owned()), addons: addons.to_owned(), + addons_locked: *addons_locked, + library_missing: *library_missing, settings: Settings::default(), }; if *profile != next_profile { @@ -285,6 +302,9 @@ pub fn update_profile( result, )) if profile.auth_key() == Some(auth_key) => match result { Ok(addons) => { + // on successful AddonsApi result, unlock the addons if they have been locked + profile.addons_locked = false; + let prev_transport_urls = profile .addons .iter() diff --git a/src/models/ctx/update_search_history.rs b/src/models/ctx/update_search_history.rs index f6cb746ce..9aa64c4b2 100644 --- a/src/models/ctx/update_search_history.rs +++ b/src/models/ctx/update_search_history.rs @@ -3,7 +3,7 @@ use futures::FutureExt; use crate::constants::SEARCH_HISTORY_STORAGE_KEY; use crate::models::ctx::{CtxError, CtxStatus}; -use crate::runtime::msg::{Action, ActionCtx, Event, Internal}; +use crate::runtime::msg::{Action, ActionCtx, CtxAuthResponse, Event, Internal}; use crate::runtime::{Effect, EffectFuture, Effects, Env, EnvFutureExt}; use crate::{runtime::msg::Msg, types::search_history::SearchHistoryBucket}; @@ -27,7 +27,7 @@ pub fn update_search_history( Effects::msg(Msg::Internal(Internal::SearchHistoryChanged)) } Msg::Internal(Internal::CtxAuthResult(auth_request, result)) => match (status, result) { - (CtxStatus::Loading(loading_auth_request), Ok((auth, ..))) + (CtxStatus::Loading(loading_auth_request), Ok(CtxAuthResponse { auth, .. })) if loading_auth_request == auth_request => { let next_search_history = SearchHistoryBucket::new(Some(auth.user.id.to_owned())); diff --git a/src/models/ctx/update_streams.rs b/src/models/ctx/update_streams.rs index b3b9cdeee..1c4273e54 100644 --- a/src/models/ctx/update_streams.rs +++ b/src/models/ctx/update_streams.rs @@ -5,7 +5,7 @@ use std::collections::hash_map::Entry; use crate::constants::STREAMS_STORAGE_KEY; use crate::models::common::{Loadable, ResourceLoadable}; use crate::models::ctx::{CtxError, CtxStatus}; -use crate::runtime::msg::{Action, ActionCtx, Event, Internal, Msg}; +use crate::runtime::msg::{Action, ActionCtx, CtxAuthResponse, Event, Internal, Msg}; use crate::runtime::{Effect, EffectFuture, Effects, Env, EnvFutureExt}; use crate::types::streams::{StreamsBucket, StreamsItem, StreamsItemKey}; @@ -85,7 +85,7 @@ pub fn update_streams( Effects::one(push_streams_to_storage::(streams)).unchanged() } Msg::Internal(Internal::CtxAuthResult(auth_request, result)) => match (status, result) { - (CtxStatus::Loading(loading_auth_request), Ok((auth, _, _))) + (CtxStatus::Loading(loading_auth_request), Ok(CtxAuthResponse { auth, .. })) if loading_auth_request == auth_request => { let next_streams = StreamsBucket::new(Some(auth.user.id.to_owned())); diff --git a/src/runtime/msg/internal.rs b/src/runtime/msg/internal.rs index 7fd43dbbe..a47df7655 100644 --- a/src/runtime/msg/internal.rs +++ b/src/runtime/msg/internal.rs @@ -23,7 +23,19 @@ pub type CtxStorageResponse = ( Option, ); -pub type AuthResponse = (Auth, Vec, Vec); +#[derive(Debug)] +pub struct CtxAuthResponse { + pub auth: Auth, + pub addons: Vec, + /// If the addon get request fails, this flag will be `true` + /// to disallow the user to install addons and override his addons in the API + pub addons_locked: bool, + pub library_items: Vec, + /// If the Library datastore Get request fails on initial logging + /// this flag will be set to `true` to indicate why the user doesn't see + /// their library items. + pub library_missing: bool, +} pub type LibraryPlanResponse = (Vec, Vec); @@ -33,7 +45,7 @@ pub type LibraryPlanResponse = (Vec, Vec); #[derive(Debug)] pub enum Internal { /// Result for authenticate to API. - CtxAuthResult(AuthRequest, Result), + CtxAuthResult(AuthRequest, Result), /// Result for pull addons from API. AddonsAPIResult(APIRequest, Result, CtxError>), /// Result for pull user from API. diff --git a/src/types/profile/profile.rs b/src/types/profile/profile.rs index 8e6b46cd6..cf0ed3775 100644 --- a/src/types/profile/profile.rs +++ b/src/types/profile/profile.rs @@ -19,6 +19,13 @@ pub struct Profile { pub auth: Option, #[serde_as(deserialize_as = "UniqueVec, DescriptorUniqueVecAdapter>")] pub addons: Vec, + /// This locking flag is raised when the API addon fetch request has failed + /// in order to avoid overwriting the user's addons in the API + /// if they install a new addon locally when we have defaulted to the official ones + pub addons_locked: bool, + /// This missing library flag is raised when the API Library Collection fetch request on login + /// has failed to indicate why the user has an empty Library + pub library_missing: bool, pub settings: Settings, } @@ -27,6 +34,8 @@ impl Default for Profile { Profile { auth: None, addons: OFFICIAL_ADDONS.to_owned(), + addons_locked: false, + library_missing: false, settings: Settings::default(), } }