/* This file is part of DarkFi (https://dark.fi) * * Copyright (C) 2020-2026 Dyne.org foundation * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ use std::{ io::Cursor, sync::{Arc, Mutex as SyncMutex}, }; use darkfi_serial::{Decodable, Encodable}; use crate::{ db::AppDbPtr, error::{Error, Result}, prop::{ Property, PropertyAtomicGuard, PropertyPermission, PropertyPtr, PropertySubType, PropertyType, PropertyValue, Role, }, scene::{CallArgType, Pimpl, SceneNode, SceneNodeType, SceneNodeWeak}, theme, ExecutorPtr, }; /// Settings when modified are persisted otherwise they use their default /// as expected by properties. /// /// Settings can be used directly by other nodes or as a dependency which /// can be used in expressions. /// /// Settings are set by the user with `Role::User`. /// /// For example `net.enable_tor` might be implicitly used by `/plugin/darkirc` /// while some other setting might be used in the schema itself with the node /// not being aware its depending on an external property. /// /// In both cases modifying the setting should propagate the changes to that node. /// /// Although the `/setting` root has no knowledge of property paths underneath there /// is a convention of using `foo.bar.baz` to namespace the settings. /// Testnet endpoint from drk_config.toml const DARKFID_ENDPOINT_TCP: &str = "tcp+tls://node0.testnet.dark.fi:18345"; /// TODO: replace with the real darkfid tor endpoint const DARKFID_ENDPOINT_TOR: &str = "tor://darkfid-tor-placeholder.onion:18345"; pub fn create_setting(name: &str) -> SceneNode { let mut node = SceneNode::new(name, SceneNodeType::Setting); let mut prop = Property::new( "chat.is_enabled", PropertyType::Bool, PropertySubType::Flag, PropertyPermission::default(), ); prop.set_defaults_bool(vec![true]).unwrap(); node.add_property(prop).unwrap(); let mut prop = Property::new( "net.transport", PropertyType::Enum, PropertySubType::Null, PropertyPermission::default(), ); prop.set_enum_items(vec!["tcp", "tor"]).unwrap(); prop.set_default_enum(0, "tcp").unwrap(); node.add_property(prop).unwrap(); let mut prop = Property::new( "wallet.tcp.rpc", PropertyType::Str, PropertySubType::Null, PropertyPermission::default(), ); prop.set_defaults_str(vec![DARKFID_ENDPOINT_TCP.to_string()]).unwrap(); node.add_property(prop).unwrap(); let mut prop = Property::new( "wallet.tor.rpc", PropertyType::Str, PropertySubType::Null, PropertyPermission::default(), ); prop.set_defaults_str(vec![DARKFID_ENDPOINT_TOR.to_string()]).unwrap(); node.add_property(prop).unwrap(); let mut prop = Property::new( "theme", PropertyType::Enum, PropertySubType::Null, PropertyPermission::default(), ); prop.set_enum_items(theme::registry().iter().map(|t| t.name()).collect::>()).unwrap(); // Default scifi: the shipped look. A proper Enum variant default // (set_default_enum), unlike the Str-on-Enum latent mismatch above. prop.set_default_enum(0, theme::DEFAULT_THEME).unwrap(); node.add_property(prop).unwrap(); let mut prop = Property::new( "win.scale", PropertyType::Float32, PropertySubType::Null, PropertyPermission::default(), ); prop.set_defaults_f32(vec![1.]).unwrap(); prop.set_range_f32(0., f32::MAX); node.add_property(prop).unwrap(); node.add_method( "search", vec![("filter", "Filter string to search keys", CallArgType::Str)], None, ) .unwrap(); node } pub type SettingPtr = Arc; pub struct Setting { tasks: SyncMutex>>, } impl Setting { pub async fn new(node: SceneNodeWeak, app_db: AppDbPtr, ex: ExecutorPtr) -> Pimpl { let node_ref = node.upgrade().unwrap(); // Load any persisted properties from the db in a fail-safe way let rows = match app_db.settings_all().await { Ok(rows) => rows, Err(e) => { error!(target: "app::setting", "Failed to read settings from db, using defaults: {e}"); vec![] } }; for (name, idx, data) in rows { let Some(prop) = node_ref.get_property(&name) else { warn!(target: "app::setting", "Skipping unknown persisted setting: {name}"); continue }; if let Err(e) = Self::load_prop(&prop, idx, &data) { warn!(target: "app::setting", "Skipping persisted setting {name}[{idx}]: {e}"); } } // Spawn tasks persisting our properties to the db when they change. let mut tasks = vec![]; for prop in &node_ref.props { let app_db2 = app_db.clone(); let prop2 = prop.clone(); let on_modify_sub = prop.subscribe_modify(); let task = ex.spawn(async move { while let Ok((_role, _action, _guard)) = on_modify_sub.receive().await { Self::save_prop(&prop2, &app_db2).await.unwrap(); } }); tasks.push(task); } Pimpl::Setting(Arc::new(Self { tasks: SyncMutex::new(tasks) })) } /// Persist the state of a property: one row per set index keyed by /// (prop name, idx). Unset indexes have their row deleted. async fn save_prop(prop: &PropertyPtr, app_db: &AppDbPtr) -> Result<()> { assert!(prop.is_bounded()); for i in 0..prop.get_len() { if prop.is_unset(i)? { app_db.setting_remove_idx(&prop.name, i as u32).await?; continue } let val = prop.get_value(i)?; let mut data = vec![]; Self::encode_value(prop, &val, &mut data)?; app_db.setting_put(&prop.name, i as u32, "prop", &data).await?; } Ok(()) } /// Serialize a single value. The property type determines the binary /// format of the value itself. If the property allows null values then /// it is written as an option (`Option`): a single tag byte followed /// by the value only when it is not null. fn encode_value(prop: &Property, val: &PropertyValue, data: &mut Vec) -> Result<()> { if prop.is_null_allowed { match val { PropertyValue::Null => { false.encode(data)?; } val => { true.encode(data)?; val.encode(data)?; } } } else { val.encode(data)?; } Ok(()) } /// Restore the value of a property at `idx` previously written by /// `Self::save_prop()`. fn load_prop(prop: &PropertyPtr, idx: u32, data: &[u8]) -> Result<()> { assert!(prop.is_bounded()); let mut cur = Cursor::new(data); let atom = &mut PropertyAtomicGuard::none(); let val = Self::decode_value(prop, &mut cur)?; Self::apply_value(prop, atom, idx as usize, val)?; Ok(()) } /// Decode a value serialized by `Self::encode_value()`. fn decode_value(prop: &Property, cur: &mut Cursor<&[u8]>) -> Result { macro_rules! decode_ty { ($typ:ty, $variant:ident) => {{ if prop.is_null_allowed { match Option::<$typ>::decode(cur)? { Some(v) => PropertyValue::$variant(v), None => PropertyValue::Null, } } else { PropertyValue::$variant(<$typ>::decode(cur)?) } }}; } let val = match prop.typ { PropertyType::Bool => decode_ty!(bool, Bool), PropertyType::Uint32 => decode_ty!(u32, Uint32), PropertyType::Float32 => decode_ty!(f32, Float32), PropertyType::Str => decode_ty!(String, Str), PropertyType::Enum => decode_ty!(String, Enum), PropertyType::SceneNodeId => decode_ty!(u32, SceneNodeId), PropertyType::Null | PropertyType::SExpr | PropertyType::VectorShape => { return Err(Error::PropertyWrongType) } }; Ok(val) } /// Set the value at index `i` as `Role::User`. fn apply_value( prop: &PropertyPtr, atom: &mut PropertyAtomicGuard, i: usize, val: PropertyValue, ) -> Result<()> { match val { PropertyValue::Bool(v) => prop.set_bool(atom, Role::User, i, v), PropertyValue::Uint32(v) => prop.set_u32(atom, Role::User, i, v), PropertyValue::Float32(v) => prop.set_f32(atom, Role::User, i, v), PropertyValue::Str(v) => prop.set_str(atom, Role::User, i, v), PropertyValue::Enum(v) => prop.set_enum(atom, Role::User, i, v), PropertyValue::SceneNodeId(v) => prop.set_node_id(atom, Role::User, i, v), PropertyValue::Null => prop.set_null(atom, Role::User, i), PropertyValue::Unset | PropertyValue::SExpr(_) | PropertyValue::VectorShape(_) => { Err(Error::PropertyWrongType) } } } } impl Drop for Setting { fn drop(&mut self) { self.tasks.lock().unwrap().clear(); } }