use std::fmt::Display; use std::sync::Arc; use anyhow::Result; use regex::Regex; use warp_core::features::FeatureFlag; use warp_core::report_if_error; use warp_core::user_preferences::GetUserPreferences as _; use warpui::{AppContext, Entity, ModelContext, SingletonEntity, UpdateModel}; use crate::ai::blocklist::telemetry_banner::should_collect_ai_ugc_telemetry; use crate::auth::auth_state::AuthState; use crate::auth::AuthStateProvider; use crate::cloud_object::model::persistence::CloudModel; use crate::report_error; use crate::server::cloud_objects::update_manager::UpdateManager; #[cfg(test)] use crate::server::server_api::auth::MockAuthClient; use crate::server::server_api::auth::{AuthClient, SyncedUserSettings}; use crate::server::server_api::ServerApiProvider; use crate::terminal::safe_mode_settings::SafeModeSettings; use settings::{ macros::{define_settings_group, maybe_define_setting, register_settings_events}, RespectUserSyncSetting, Setting, SupportedPlatforms, SyncToCloud, }; use serde::{Deserialize, Serialize}; use super::cloud_preferences_syncer::CloudPreferencesSyncer; use crate::workspaces::workspace::EnterpriseSecretRegex; pub trait RegexDisplayInfo { fn pattern(&self) -> &str; fn name(&self) -> Option<&str>; } pub const TELEMETRY_ENABLED_DEFAULTS_KEY: &str = "TelemetryEnabled"; pub const CRASH_REPORTING_ENABLED_DEFAULTS_KEY: &str = "CrashReportingEnabled"; pub const CLOUD_CONVERSATION_STORAGE_ENABLED_DEFAULTS_KEY: &str = "CloudConversationStorageEnabled"; #[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)] #[schemars(description = "A custom regex pattern for detecting and redacting secrets.")] pub struct CustomSecretRegex { #[serde(with = "serde_regex")] #[schemars(with = "String", description = "The regex pattern to match secrets.")] pub pattern: Regex, #[serde(default)] #[schemars(description = "Optional display name for this secret pattern.")] pub name: Option, } impl CustomSecretRegex { pub fn pattern(&self) -> &Regex { &self.pattern } } impl RegexDisplayInfo for CustomSecretRegex { fn pattern(&self) -> &str { self.pattern.as_str() } fn name(&self) -> Option<&str> { self.name.as_deref() } } impl RegexDisplayInfo for EnterpriseSecretRegex { fn pattern(&self) -> &str { &self.pattern } fn name(&self) -> Option<&str> { self.name.as_deref() } } impl Display for CustomSecretRegex { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{}", self.pattern.as_str()) } } impl PartialEq for CustomSecretRegex { /// We do not factor in the name to equality checks -- /// if the regex is the same, then the regex is the same. /// This allows us to avoid adding duplicate regexes. fn eq(&self, other: &Self) -> bool { self.pattern.as_str() == other.pattern.as_str() } } impl settings_value::SettingsValue for CustomSecretRegex {} define_settings_group!(WarpDrivePrivacySettings, settings: [ is_telemetry_enabled: IsTelemetryEnabled { type: bool, default: true, supported_platforms: SupportedPlatforms::ALL, sync_to_cloud: SyncToCloud::Globally(RespectUserSyncSetting::No), private: false, storage_key: "TelemetryEnabled", toml_path: "privacy.telemetry_enabled", description: "Whether anonymous usage telemetry is collected.", }, is_crash_reporting_enabled: IsCrashReportingEnabled { type: bool, default: true, supported_platforms: SupportedPlatforms::ALL, sync_to_cloud: SyncToCloud::Globally(RespectUserSyncSetting::No), private: false, storage_key: "CrashReportingEnabled", toml_path: "privacy.crash_reporting_enabled", description: "Whether crash reports are sent.", }, is_cloud_conversation_storage_enabled: IsCloudConversationStorageEnabled { type: bool, default: true, supported_platforms: SupportedPlatforms::ALL, sync_to_cloud: SyncToCloud::Globally(RespectUserSyncSetting::No), private: false, storage_key: "CloudConversationStorageEnabled", toml_path: "agents.cloud_conversation_storage_enabled", description: "Whether conversations are stored in the cloud.", }, ]); maybe_define_setting!(CustomSecretRegexList, group: PrivacySettings, { type: Vec, default: Vec::new(), supported_platforms: SupportedPlatforms::ALL, sync_to_cloud: SyncToCloud::Globally(RespectUserSyncSetting::No), private: false, toml_path: "privacy.custom_secret_regex_list", description: "Custom regex patterns for detecting and redacting secrets.", }); maybe_define_setting!(HasInitializedDefaultSecretRegexes, group: PrivacySettings, { type: bool, default: false, supported_platforms: SupportedPlatforms::ALL, sync_to_cloud: SyncToCloud::Globally(RespectUserSyncSetting::No), private: true, }); /// Singleton model for managing the user's privacy settings (whether the user has enabled crash /// reporting and/or telemetry). pub struct PrivacySettings { auth_state: Arc, auth_client: Arc, pub is_telemetry_enabled: bool, pub is_crash_reporting_enabled: bool, pub is_cloud_conversation_storage_enabled: bool, pub has_initialized_default_secret_regexes: HasInitializedDefaultSecretRegexes, /// List of user defined secret regexes. /// Enterprise-level secret regexes will always take precedence over user-level secrets, /// but they both used to support additive behavior. /// It's a [Vec], but also a user setting. pub user_secret_regex_list: CustomSecretRegexList, /// List of enterprise-level secret regexes provided by the organization. /// These are kept separate from user-level secrets to support additive behavior. pub enterprise_secret_regex_list: Vec, /// Whether or not the user's organization has forced telemetry on, in which case we ignore any /// user local/cloud settings. If false, we fall back to the user's settings. /// This is populated by the server when teams data is fetched. pub is_telemetry_force_enabled: bool, /// Whether or not the user's organization has enabled enterprise secret redaction. /// This is populated by the server when teams data is fetched. pub is_enterprise_secret_redaction_enabled: bool, } /// A snapshot of a user's [`PrivacySettings`] settings at some point in time. #[derive(Clone, Copy)] pub struct PrivacySettingsSnapshot { is_telemetry_enabled: bool, is_crash_reporting_enabled: bool, is_telemetry_force_enabled: bool, should_collect_ai_ugc_telemetry: bool, // This is an option so that, if a user has not set this value (and it's set to its default value of true), // the default value won't override a value that the user previously set on a different device. // This is set to a non-option once the user manually changes this setting. cloud_conversation_storage_enabled: Option, } impl PrivacySettingsSnapshot { pub fn cloud_conversation_storage_enabled(&self) -> Option { self.cloud_conversation_storage_enabled } pub fn is_telemetry_enabled(&self) -> bool { self.is_telemetry_enabled } pub fn is_crash_reporting_enabled(&self) -> bool { self.is_crash_reporting_enabled } pub fn is_telemetry_force_enabled(&self) -> bool { self.is_telemetry_force_enabled } pub fn should_disable_telemetry(&self) -> bool { // If a user has opted in to the agent mode analytics experiment, telemetry must be enabled. !self.is_telemetry_enabled && !self.is_telemetry_force_enabled && !FeatureFlag::AgentModeAnalytics.is_enabled() } pub fn should_collect_ai_ugc_telemetry(&self) -> bool { self.should_collect_ai_ugc_telemetry } #[cfg(test)] pub fn mock() -> Self { Self { cloud_conversation_storage_enabled: None, is_telemetry_enabled: true, is_crash_reporting_enabled: true, is_telemetry_force_enabled: true, should_collect_ai_ugc_telemetry: true, } } } impl PrivacySettings { /// Registers a singleton PrivacySettings model on `app`. /// /// We expose this function publicly (while keeping the constructor private) to prevent /// instantiation another PrivacySettings struct, in the case where a developer might be /// unaware that it is registered as a singleton model. pub fn register_singleton(ctx: &mut AppContext) { let handle = ctx.add_singleton_model(PrivacySettings::new); register_settings_events!( PrivacySettings, user_secret_regex_list, CustomSecretRegexList, handle, ctx ); } /// Returns a new PrivacySettings object initialized from locally cached values. Server-side /// settings are fetched later via `fetch_or_update_settings`, which is called from /// `on_user_fetched` after the user's auth state is established. fn new(ctx: &mut ModelContext) -> Self { let auth_state = AuthStateProvider::as_ref(ctx).get().clone(); let auth_client = ServerApiProvider::as_ref(ctx).get_auth_client(); let is_telemetry_enabled: bool = ctx .private_user_preferences() .read_value(TELEMETRY_ENABLED_DEFAULTS_KEY) .unwrap_or_default() .and_then(|s| serde_json::from_str(&s).ok()) .unwrap_or(true); let is_crash_reporting_enabled: bool = ctx .private_user_preferences() .read_value(CRASH_REPORTING_ENABLED_DEFAULTS_KEY) .unwrap_or_default() .and_then(|s| serde_json::from_str(&s).ok()) .unwrap_or(true); let is_cloud_conversation_storage_enabled: bool = ctx .private_user_preferences() .read_value(CLOUD_CONVERSATION_STORAGE_ENABLED_DEFAULTS_KEY) .unwrap_or_default() .and_then(|s| serde_json::from_str(&s).ok()) .unwrap_or(true); // Make sure the user-preferences stores match what's in memory. // Needed for warp drive preferences to work and no harm in doing in general. let _ = ctx.private_user_preferences().write_value( TELEMETRY_ENABLED_DEFAULTS_KEY, serde_json::to_string(&is_telemetry_enabled) .expect("is_telemetry_enabled is a boolean."), ); let _ = ctx.private_user_preferences().write_value( CRASH_REPORTING_ENABLED_DEFAULTS_KEY, serde_json::to_string(&is_crash_reporting_enabled) .expect("is_crash_reporting_enabled is a boolean."), ); let _ = ctx.private_user_preferences().write_value( CLOUD_CONVERSATION_STORAGE_ENABLED_DEFAULTS_KEY, serde_json::to_string(&is_cloud_conversation_storage_enabled) .expect("is_cloud_conversation_storage_enabled is a boolean."), ); // Listen for changes to the cloud model and update ourselves when they happen. ctx.subscribe_to_model(&WarpDrivePrivacySettings::handle(ctx), |me, event, ctx| { let privacy_settings = WarpDrivePrivacySettings::as_ref(ctx); match event { WarpDrivePrivacySettingsChangedEvent::IsTelemetryEnabled { .. } => { me.set_is_telemetry_enabled( *privacy_settings.is_telemetry_enabled.value(), ctx, ); } WarpDrivePrivacySettingsChangedEvent::IsCrashReportingEnabled { .. } => { me.set_is_crash_reporting_enabled( *privacy_settings.is_crash_reporting_enabled.value(), ctx, ); } WarpDrivePrivacySettingsChangedEvent::IsCloudConversationStorageEnabled { .. } => { me.set_is_cloud_conversation_storage_enabled( *privacy_settings .is_cloud_conversation_storage_enabled .value(), ctx, ); } } }); let user_secret_regex_list: CustomSecretRegexList = CustomSecretRegexList::new_from_storage(ctx); let has_initialized_default_secret_regexes: HasInitializedDefaultSecretRegexes = HasInitializedDefaultSecretRegexes::new_from_storage(ctx); Self { auth_state, auth_client, is_crash_reporting_enabled, is_telemetry_enabled, is_cloud_conversation_storage_enabled, user_secret_regex_list, has_initialized_default_secret_regexes, is_telemetry_force_enabled: false, is_enterprise_secret_redaction_enabled: false, enterprise_secret_regex_list: Vec::new(), } } pub fn is_telemetry_force_enabled(&self) -> bool { self.is_telemetry_force_enabled } pub fn set_is_telemetry_force_enabled(&mut self, is_telemetry_force_enabled: bool) { self.is_telemetry_force_enabled = is_telemetry_force_enabled; } pub fn is_enterprise_secret_redaction_enabled(&self) -> bool { self.is_enterprise_secret_redaction_enabled } pub fn set_enterprise_secret_redaction_settings( &mut self, enabled: bool, enterprise_regexes: Vec, change_event_reason: ChangeEventReason, ctx: &mut ModelContext, ) { if enabled { // First time: Force enable secret redaction setting (safe mode). if !self.is_enterprise_secret_redaction_enabled { let safe_mode_settings = SafeModeSettings::handle(ctx); ctx.update_model(&safe_mode_settings, |safe_mode_settings, ctx| { let _ = safe_mode_settings.safe_mode_enabled.set_value(true, ctx); }); } // Convert EnterpriseSecretRegex to CustomSecretRegex for internal use let mut enterprise_secrets = Vec::new(); for enterprise_regex in enterprise_regexes { if let Ok(regex) = Regex::new(&enterprise_regex.pattern) { enterprise_secrets.push(CustomSecretRegex { pattern: regex, name: enterprise_regex.name, }); } else { log::error!( "Invalid enterprise secret regex pattern: {}", enterprise_regex.pattern ); } } self.enterprise_secret_regex_list = enterprise_secrets; } else { // Clear enterprise secrets when disabled self.enterprise_secret_regex_list.clear(); } self.is_enterprise_secret_redaction_enabled = enabled; ctx.emit(PrivacySettingsChangedEvent::CustomSecretRegexList { change_event_reason, }); ctx.notify(); } pub fn refresh_to_default(&mut self) { // TODO(zach): this seems incorrect - should we also update the values on disk? self.is_telemetry_enabled = true; self.is_crash_reporting_enabled = true; self.is_cloud_conversation_storage_enabled = true; self.is_telemetry_force_enabled = false; self.is_enterprise_secret_redaction_enabled = false; } /// Fetch the user's privacy settings from the server if any or update the server settings. pub fn fetch_or_update_settings(&self, ctx: &mut ModelContext) { let auth_client_clone = self.auth_client.clone(); let _ = ctx.spawn( async move { auth_client_clone.get_user_settings().await }, Self::initialize_from_fetched_settings_or_update_settings, ); } /// Initializes state from the [`SyncedUserSettings`] fetched from the server, if any. /// If there are no settings from the server, updates the server settings with local settings. /// TODO: Make this a server-side db transaction. fn initialize_from_fetched_settings_or_update_settings( &mut self, fetched_settings: Result>, ctx: &mut ModelContext, ) { match fetched_settings { Ok(Some(fetched_settings)) => { // Until the login experience stops hiding the telemetry settings, // we assume that locally enabled telemetry is unintentional. // As such, where settings differ, we respect whichever setting that is disabled. self.overwrite_local_settings_if_cloud_disabled(fetched_settings, ctx); // If any local setting is disabled, we have to update the server. if !self.is_telemetry_enabled || !self.is_crash_reporting_enabled || !self.is_cloud_conversation_storage_enabled { self.update_server_with_local_settings(ctx); } } Ok(None) => { // This indicates the user had not logged in before. log::info!("User has no synced privacy settings."); self.update_server_with_local_settings(ctx); } Err(err) => { report_error!(err.context("Failed to fetch user settings.")); } } self.maybe_sync_with_warp_drive_prefs(ctx); } fn overwrite_local_settings_if_cloud_disabled( &mut self, fetched_settings: SyncedUserSettings, ctx: &mut ModelContext, ) { // For now, only overwrite the user's locally stored setting if the cloud version // has is_crash_reporting disabled. Until we implement a more reliable retry // mechanism for update settings requests, in addition to possibly a UI for the // user to resolve the conflicting settings themselves, default to "safe" behavior. // Namely, we want to avoid incidentally overwriting is_crash_reporting_enabled to // `true`. if self.is_crash_reporting_enabled && !fetched_settings.is_crash_reporting_enabled { self.set_is_crash_reporting_enabled(fetched_settings.is_crash_reporting_enabled, ctx); } // For now, only overwrite the user's locally stored setting if the cloud version // has is_telemetry_enabled disabled. Until we implement a more reliable retry // mechanism for update settings requests, in addition to possibly a UI for the // user to resolve the conflicting settings themselves, default to "safe" behavior. // Namely, we want to avoid incidentally overwriting is_telemetry_enabled to // `true`. if self.is_telemetry_enabled && !fetched_settings.is_telemetry_enabled { self.set_is_telemetry_enabled(fetched_settings.is_telemetry_enabled, ctx); } if self.is_cloud_conversation_storage_enabled && !fetched_settings.is_cloud_conversation_storage_enabled { self.set_is_cloud_conversation_storage_enabled( fetched_settings.is_cloud_conversation_storage_enabled, ctx, ); } } /// Constructor for tests only. #[cfg(test)] pub fn mock(_ctx: &mut ModelContext) -> Self { Self { auth_state: Arc::new(AuthState::new_for_test()), auth_client: Arc::new(MockAuthClient::new()), is_crash_reporting_enabled: true, is_telemetry_enabled: true, is_cloud_conversation_storage_enabled: true, user_secret_regex_list: CustomSecretRegexList::new(None), has_initialized_default_secret_regexes: HasInitializedDefaultSecretRegexes::new(None), is_telemetry_force_enabled: false, is_enterprise_secret_redaction_enabled: false, enterprise_secret_regex_list: Vec::new(), } } /// Returns a snapshot of the user's privacy settings. /// /// The returned snapshot is not stateful, thus its values should be used shortly after the /// snapshot is returned. pub fn get_snapshot(&self, app: &AppContext) -> PrivacySettingsSnapshot { PrivacySettingsSnapshot { cloud_conversation_storage_enabled: (!self.is_cloud_conversation_storage_enabled) .then_some(false), is_telemetry_enabled: self.is_telemetry_enabled, is_crash_reporting_enabled: self.is_crash_reporting_enabled, is_telemetry_force_enabled: self.is_telemetry_force_enabled, should_collect_ai_ugc_telemetry: should_collect_ai_ugc_telemetry( app, self.is_telemetry_enabled, ), } } /// Sets `is_crash_reporting_enabled` to the given value. /// /// Additionally, this writes the given value to the user's local defaults, and additionally /// sends a request to update the user's `is_crash_reporting_enabled` value stored server-side. /// Finally, emits a `PrivacySettingsEvent::UpdateIsCrashReportingEnabled` event. pub fn set_is_crash_reporting_enabled( &mut self, new_value: bool, ctx: &mut ModelContext, ) { let old_value = self.is_crash_reporting_enabled; if new_value != old_value { self.is_crash_reporting_enabled = new_value; WarpDrivePrivacySettings::handle(ctx).update(ctx, |settings, ctx| { log::info!("Setting is_crash_reporting_enabled to {new_value}"); let _ = settings .is_crash_reporting_enabled .set_value(new_value, ctx); }); if self.auth_state.is_logged_in() { let auth_client = self.auth_client.clone(); let _ = ctx.spawn( async move { auth_client.set_is_crash_reporting_enabled(new_value).await }, |_, _, _| (), ); } ctx.emit(PrivacySettingsChangedEvent::UpdateIsCrashReportingEnabled { old_value, new_value, }); ctx.notify(); } } /// Sets `is_telemetry_enabled` to the given value. /// /// Additionally, this writes the given value to the user's local defaults, and additionally /// sends a request to update the user's `is_telemetry_enabled` value stored server-side. /// Finally, emits a `PrivacySettingsEvent::UpdateIsTelemetryEnabled` event. pub fn set_is_telemetry_enabled( &mut self, new_value: bool, ctx: &mut ModelContext, ) { let old_value = self.is_telemetry_enabled; if new_value != old_value { self.is_telemetry_enabled = new_value; WarpDrivePrivacySettings::handle(ctx).update(ctx, |settings, ctx| { log::info!("Setting is_telemetry_enabled to {new_value}"); let _ = settings.is_telemetry_enabled.set_value(new_value, ctx); }); if self.auth_state.is_logged_in() { let auth_client = self.auth_client.clone(); let _ = ctx.spawn( async move { auth_client.set_is_telemetry_enabled(new_value).await }, |_, _, _| (), ); } ctx.emit(PrivacySettingsChangedEvent::UpdateIsTelemetryEnabled { old_value, new_value, }); ctx.notify(); } } pub fn set_is_cloud_conversation_storage_enabled( &mut self, new_value: bool, ctx: &mut ModelContext, ) { let old_value = self.is_cloud_conversation_storage_enabled; if new_value == old_value { return; } self.is_cloud_conversation_storage_enabled = new_value; WarpDrivePrivacySettings::handle(ctx).update(ctx, |settings, ctx| { log::info!("Setting is_cloud_conversation_storage_enabled to {new_value}"); let _ = settings .is_cloud_conversation_storage_enabled .set_value(new_value, ctx); }); if self.auth_state.is_logged_in() { let auth_client = self.auth_client.clone(); let _ = ctx.spawn( async move { auth_client .set_is_cloud_conversation_storage_enabled(new_value) .await }, |_, _, _| (), ); } ctx.emit( PrivacySettingsChangedEvent::UpdateIsCloudConversationStorageEnabled { old_value, new_value, }, ); ctx.notify(); } pub fn remove_user_secret_regex(&mut self, idx: &usize, ctx: &mut ModelContext) { let mut new_user_secret_regex_list = self.user_secret_regex_list.to_vec(); new_user_secret_regex_list.remove(*idx); if self .user_secret_regex_list .set_value(new_user_secret_regex_list, ctx) .is_err() { log::error!("Custom Secret Regex List failed to serialize") } } /// Initializes the custom secret regex list with the default regexes, provided /// non matches can be found. /// This can be called when a user first enables secret redaction. pub fn add_all_recommended_regex(&mut self, ctx: &mut ModelContext) { let mut new_user_secret_regex_list = self.user_secret_regex_list.to_vec(); let num_existing_regexes = new_user_secret_regex_list.len(); // Add all the default regexes if they don't already exist for default_regex in crate::terminal::model::secrets::regexes::DEFAULT_REGEXES_WITH_NAMES { if let Ok(regex) = Regex::new(default_regex.pattern) { let custom_regex = CustomSecretRegex { pattern: regex, name: Some(default_regex.name.to_string()), }; if !new_user_secret_regex_list.contains(&custom_regex) { new_user_secret_regex_list.push(custom_regex); } } else { log::error!("Failed to compile default regex: {}", default_regex.pattern); } } if num_existing_regexes == new_user_secret_regex_list.len() { return; } if self .user_secret_regex_list .set_value(new_user_secret_regex_list, ctx) .is_err() { log::error!("Failed to serialize default regexes to custom secret regex list") } ctx.notify(); } /// Disables the default regex trigger, so that it will not be executed. pub fn disable_default_regex_trigger(&mut self, ctx: &mut ModelContext) { if self .has_initialized_default_secret_regexes .set_value(true, ctx) .is_err() { log::error!("Failed to disable default regex trigger"); } } /// Initializes the custom secret regex list with the default regexes. /// This will only be executed once per user, and only if they haven't already initialized. pub fn initialize_default_regexes_once(&mut self, ctx: &mut ModelContext) { // Only initialize if we haven't done so before if !*self.has_initialized_default_secret_regexes.value() { self.add_all_recommended_regex(ctx); // Mark as initialized if self .has_initialized_default_secret_regexes .set_value(true, ctx) .is_err() { log::error!("Failed to set has_initialized_default_secret_regexes flag"); } } } /// Sends request(s) to update server-side user settings with current local values. fn update_server_with_local_settings(&self, ctx: &mut ModelContext) { if self.auth_state.is_logged_in() { let auth_client = self.auth_client.clone(); let snapshot = self.get_snapshot(ctx); let _ = ctx.spawn( async move { let result = auth_client.update_user_settings(snapshot).await; if let Err(err) = result { report_error!( err.context("Failed to update server with local privacy settings.") ) } }, |_, _, _| (), ); } } /// We wait until warp drive prefs have loaded and then either /// 1) use them as the data store for is_telemetry_enabled and is_crash_reporting_enabled, if those /// values are set in warp drive, or /// 2) update the warp drive prefs to match the values from the legacy user_settings endpoint so /// that we can use warp drive prefs going forward. pub fn maybe_sync_with_warp_drive_prefs(&mut self, ctx: &mut ModelContext) { // Wait for cloud objects to load, and, if telemetry & crash reporting are synced to warp drive // initialize from the warp drive values. let update_manager = UpdateManager::as_ref(ctx); ctx.spawn( update_manager.initial_load_complete(), Self::handle_warp_drive_objects_loaded, ); } fn handle_warp_drive_objects_loaded(&mut self, _: (), ctx: &mut ModelContext) { self.initialize_default_regexes_once(ctx); // Check if the warp drive preferences are set. If they are, and telemetry and crash reporting // are set as warp drive prefs, then use those. Otherwise, update the warp drive prefs to match // the values from the legacy user_settings endpoint so that we can use warp drive prefs going forward. let cloud_model = CloudModel::as_ref(ctx); let cloud_prefs = cloud_model.get_all_cloud_preferences_by_storage_key(); let cloud_telemetry_value = cloud_prefs .get(IsTelemetryEnabled::storage_key()) .map(|pref| { pref.model() .string_model .value .as_bool() .unwrap_or_default() }); let cloud_crash_reporting_value = cloud_prefs .get(IsCrashReportingEnabled::storage_key()) .map(|pref| { pref.model() .string_model .value .as_bool() .unwrap_or_default() }); let cloud_conversation_storage_value = cloud_prefs .get(IsCloudConversationStorageEnabled::storage_key()) .map(|pref| { pref.model() .string_model .value .as_bool() .unwrap_or_default() }); match ( cloud_telemetry_value, cloud_crash_reporting_value, cloud_conversation_storage_value, ) { ( Some(is_telemetry_enabled), Some(is_crash_reporting_enabled), Some(is_cloud_conversation_storage_enabled), ) => { log::info!( "Warp Drive privacy preferences are set, using those for telemetry={is_telemetry_enabled}, \ crash_reporting={is_crash_reporting_enabled}, cloud_conversation_storage={is_cloud_conversation_storage_enabled}" ); self.set_is_telemetry_enabled(is_telemetry_enabled, ctx); self.set_is_crash_reporting_enabled(is_crash_reporting_enabled, ctx); self.set_is_cloud_conversation_storage_enabled( is_cloud_conversation_storage_enabled, ctx, ); } _ => { log::info!( "Warp Drive privacy preferences are not set, syncing local PrivacySettings values to \ WarpDrivePrivacySettings and cloud. telemetry={}, crash_reporting={}, \ cloud_conversation_storage={}", self.is_telemetry_enabled, self.is_crash_reporting_enabled, self.is_cloud_conversation_storage_enabled ); // First, ensure WarpDrivePrivacySettings (the define_settings_group model) // reflects the actual PrivacySettings in-memory values. These may differ // because WarpDrivePrivacySettings defaults to `true` for all three settings, // while the user may have changed them to `false` via PrivacySettings before // signing up. Without this step, maybe_sync_local_prefs_to_cloud would read // the stale WarpDrivePrivacySettings defaults and push those to the cloud. WarpDrivePrivacySettings::handle(ctx).update(ctx, |settings, ctx| { report_if_error!(settings .is_telemetry_enabled .set_value(self.is_telemetry_enabled, ctx)); report_if_error!(settings .is_crash_reporting_enabled .set_value(self.is_crash_reporting_enabled, ctx)); report_if_error!(settings .is_cloud_conversation_storage_enabled .set_value(self.is_cloud_conversation_storage_enabled, ctx)); }); CloudPreferencesSyncer::handle(ctx).update(ctx, |syncer, ctx| { syncer.maybe_sync_local_prefs_to_cloud( vec![ IsTelemetryEnabled::storage_key().to_string(), IsCrashReportingEnabled::storage_key().to_string(), IsCloudConversationStorageEnabled::storage_key().to_string(), ], ctx, ); }); } } } } /// Events emitted when PrivacySettings is updated. #[derive(Clone, Copy)] pub enum PrivacySettingsChangedEvent { UpdateIsTelemetryEnabled { old_value: bool, new_value: bool, }, UpdateIsCrashReportingEnabled { old_value: bool, new_value: bool, }, UpdateIsCloudConversationStorageEnabled { old_value: bool, new_value: bool, }, CustomSecretRegexList { change_event_reason: ChangeEventReason, }, HasInitializedDefaultSecretRegexes { change_event_reason: ChangeEventReason, }, } impl Entity for PrivacySettings { type Event = PrivacySettingsChangedEvent; } impl SingletonEntity for PrivacySettings {}