notedeck

One damus client to rule them all
git clone git://jb55.com/notedeck
Log | Files | Refs | README | LICENSE

commit 6003ef5aecb3228b9c93a7aa160e2ad4a3219600
parent d9f92ef54fb676fa39a8d35bbf9dc792f63a9423
Author: kernelkind <kernelkind@gmail.com>
Date:   Mon, 17 Mar 2025 19:36:23 -0400

`FileKeyStorage` -> `AccountStorage`

Signed-off-by: kernelkind <kernelkind@gmail.com>

Diffstat:
Mcrates/notedeck/src/accounts.rs | 30+++++++++++++++---------------
Mcrates/notedeck/src/app.rs | 4++--
Mcrates/notedeck/src/lib.rs | 2+-
Acrates/notedeck/src/storage/account_storage.rs | 163+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Dcrates/notedeck/src/storage/file_key_storage.rs | 163-------------------------------------------------------------------------------
Mcrates/notedeck/src/storage/mod.rs | 4++--
6 files changed, 183 insertions(+), 183 deletions(-)

diff --git a/crates/notedeck/src/accounts.rs b/crates/notedeck/src/accounts.rs @@ -1,7 +1,7 @@ use tracing::{debug, error, info}; use crate::{ - FileKeyStorage, MuteFun, Muted, RelaySpec, SingleUnkIdAction, UnknownIds, UserAccount, + AccountStorage, MuteFun, Muted, RelaySpec, SingleUnkIdAction, UnknownIds, UserAccount, }; use enostr::{ClientMessage, FilledKeypair, Keypair, Pubkey, RelayPool}; use nostrdb::{Filter, Ndb, Note, NoteBuilder, NoteKey, Subscription, Transaction}; @@ -308,7 +308,7 @@ pub struct AccountData { pub struct Accounts { currently_selected_account: Option<usize>, accounts: Vec<UserAccount>, - key_store: Option<FileKeyStorage>, + key_store: Option<AccountStorage>, account_data: BTreeMap<[u8; 32], AccountData>, forced_relays: BTreeSet<RelaySpec>, bootstrap_relays: BTreeSet<RelaySpec>, @@ -316,10 +316,10 @@ pub struct Accounts { } impl Accounts { - pub fn new(key_store: Option<FileKeyStorage>, forced_relays: Vec<String>) -> Self { + pub fn new(key_store: Option<AccountStorage>, forced_relays: Vec<String>) -> Self { let accounts = match &key_store { - Some(keystore) => match keystore.get_keys() { - Ok(k) => k.into_iter().map(|key| UserAccount { key }).collect(), + Some(keystore) => match keystore.get_accounts() { + Ok(k) => k, Err(e) => { tracing::error!("could not get keys: {e}"); Vec::new() @@ -434,22 +434,22 @@ impl Accounts { } #[must_use = "UnknownIdAction's must be handled. Use .process_unknown_id_action()"] - pub fn add_account(&mut self, account: Keypair) -> AddAccountAction { - let pubkey = account.pubkey; + pub fn add_account(&mut self, key: Keypair) -> AddAccountAction { + let pubkey = key.pubkey; let switch_to_index = if let Some(contains_acc) = self.contains_account(pubkey.bytes()) { - if account.secret_key.is_some() && !contains_acc.has_nsec { + if key.secret_key.is_some() && !contains_acc.has_nsec { info!( "user provided nsec, but we already have npub {}. Upgrading to nsec", pubkey ); if let Some(key_store) = &self.key_store { - if let Err(e) = key_store.add_key(&account) { - tracing::error!("Could not add key for {:?}: {e}", account.pubkey); + if let Err(e) = key_store.write_account(&UserAccount::new(key.clone())) { + tracing::error!("Could not add key for {:?}: {e}", key.pubkey); } } - self.accounts[contains_acc.index].key = account; + self.accounts[contains_acc.index].key = key; } else { info!("already have account, not adding {}", pubkey); } @@ -457,11 +457,11 @@ impl Accounts { } else { info!("adding new account {}", pubkey); if let Some(key_store) = &self.key_store { - if let Err(e) = key_store.add_key(&account) { - tracing::error!("Could not add key for {:?}: {e}", account.pubkey); + if let Err(e) = key_store.write_account(&UserAccount::new(key.clone())) { + tracing::error!("Could not add key for {:?}: {e}", key.pubkey); } } - self.accounts.push(UserAccount::new(account)); + self.accounts.push(UserAccount::new(key)); self.accounts.len() - 1 }; @@ -821,7 +821,7 @@ enum RelayAction { Remove, } -fn get_selected_index(accounts: &[UserAccount], keystore: &FileKeyStorage) -> Option<usize> { +fn get_selected_index(accounts: &[UserAccount], keystore: &AccountStorage) -> Option<usize> { match keystore.get_selected_key() { Ok(Some(pubkey)) => { return accounts diff --git a/crates/notedeck/src/app.rs b/crates/notedeck/src/app.rs @@ -1,6 +1,6 @@ use crate::persist::{AppSizeHandler, ZoomHandler}; use crate::{ - Accounts, AppContext, Args, DataPath, DataPathType, Directory, FileKeyStorage, Images, + AccountStorage, Accounts, AppContext, Args, DataPath, DataPathType, Directory, Images, NoteCache, RelayDebugView, ThemeHandler, UnknownIds, }; use egui::ThemePreference; @@ -149,7 +149,7 @@ impl Notedeck { let keystore = if parsed_args.use_keystore { let keys_path = path.path(DataPathType::Keys); let selected_key_path = path.path(DataPathType::SelectedKey); - Some(FileKeyStorage::new( + Some(AccountStorage::new( Directory::new(keys_path), Directory::new(selected_key_path), )) diff --git a/crates/notedeck/src/lib.rs b/crates/notedeck/src/lib.rs @@ -44,7 +44,7 @@ pub use persist::*; pub use relay_debug::RelayDebugView; pub use relayspec::RelaySpec; pub use result::Result; -pub use storage::{DataPath, DataPathType, Directory, FileKeyStorage}; +pub use storage::{AccountStorage, DataPath, DataPathType, Directory}; pub use style::NotedeckTextStyle; pub use theme::ColorTheme; pub use time::time_ago_since; diff --git a/crates/notedeck/src/storage/account_storage.rs b/crates/notedeck/src/storage/account_storage.rs @@ -0,0 +1,163 @@ +use crate::{Result, UserAccount}; +use enostr::{Keypair, Pubkey, SerializableKeypair}; +use tokenator::{TokenParser, TokenSerializable, TokenWriter}; + +use super::file_storage::{delete_file, write_file, Directory}; + +static SELECTED_PUBKEY_FILE_NAME: &str = "selected_pubkey"; + +/// An OS agnostic file key storage implementation +#[derive(Debug, PartialEq)] +pub struct AccountStorage { + accounts_directory: Directory, + selected_key_directory: Directory, +} + +impl AccountStorage { + pub fn new(accounts_directory: Directory, selected_key_directory: Directory) -> Self { + Self { + accounts_directory, + selected_key_directory, + } + } + + pub fn write_account(&self, account: &UserAccount) -> Result<()> { + let mut writer = TokenWriter::new("\t"); + account.serialize_tokens(&mut writer); + write_file( + &self.accounts_directory.file_path, + account.key.pubkey.hex(), + writer.str(), + ) + } + + pub fn get_accounts(&self) -> Result<Vec<UserAccount>> { + let keys = self + .accounts_directory + .get_files()? + .values() + .filter_map(|str_key| deserialize_kp(str_key).ok()) + .map(UserAccount::new) + .collect(); + Ok(keys) + } + + pub fn remove_key(&self, key: &Keypair) -> Result<()> { + delete_file(&self.accounts_directory.file_path, key.pubkey.hex()) + } + + pub fn get_selected_key(&self) -> Result<Option<Pubkey>> { + match self + .selected_key_directory + .get_file(SELECTED_PUBKEY_FILE_NAME.to_owned()) + { + Ok(pubkey_str) => Ok(Some(serde_json::from_str(&pubkey_str)?)), + Err(crate::Error::Io(_)) => Ok(None), + Err(e) => Err(e), + } + } + + pub fn select_key(&self, pubkey: Option<Pubkey>) -> Result<()> { + if let Some(pubkey) = pubkey { + write_file( + &self.selected_key_directory.file_path, + SELECTED_PUBKEY_FILE_NAME.to_owned(), + &serde_json::to_string(&pubkey.hex())?, + ) + } else if self + .selected_key_directory + .get_file(SELECTED_PUBKEY_FILE_NAME.to_owned()) + .is_ok() + { + // Case where user chose to have no selected pubkey, but one already exists + Ok(delete_file( + &self.selected_key_directory.file_path, + SELECTED_PUBKEY_FILE_NAME.to_owned(), + )?) + } else { + Ok(()) + } + } +} + +fn deserialize_kp(serialized: &str) -> Result<Keypair> { + let data = serialized.split("\t").collect::<Vec<&str>>(); + let mut parser = TokenParser::new(&data); + + if let Ok(kp) = Keypair::parse_from_tokens(&mut parser) { + return Ok(kp); + } + + Ok(serde_json::from_str::<SerializableKeypair>(serialized)?.to_keypair("")) +} + +#[cfg(test)] +mod tests { + use std::path::PathBuf; + + use super::Result; + use super::*; + + static CREATE_TMP_DIR: fn() -> Result<PathBuf> = + || Ok(tempfile::TempDir::new()?.path().to_path_buf()); + + impl AccountStorage { + fn mock() -> Result<Self> { + Ok(Self { + accounts_directory: Directory::new(CREATE_TMP_DIR()?), + selected_key_directory: Directory::new(CREATE_TMP_DIR()?), + }) + } + } + + #[test] + fn test_basic() { + let kp = enostr::FullKeypair::generate().to_keypair(); + let storage = AccountStorage::mock().unwrap(); + let resp = storage.write_account(&UserAccount::new(kp.clone())); + + assert!(resp.is_ok()); + assert_num_storage(&storage.get_accounts(), 1); + + assert!(storage.remove_key(&kp).is_ok()); + assert_num_storage(&storage.get_accounts(), 0); + } + + fn assert_num_storage(keys_response: &Result<Vec<UserAccount>>, n: usize) { + match keys_response { + Ok(keys) => { + assert_eq!(keys.len(), n); + } + Err(_e) => { + panic!("could not get keys"); + } + } + } + + #[test] + fn test_select_key() { + let kp = enostr::FullKeypair::generate().to_keypair(); + + let storage = AccountStorage::mock().unwrap(); + let _ = storage.write_account(&UserAccount::new(kp.clone())); + assert_num_storage(&storage.get_accounts(), 1); + + let resp = storage.select_key(Some(kp.pubkey)); + assert!(resp.is_ok()); + + let resp = storage.get_selected_key(); + + assert!(resp.is_ok()); + } + + #[test] + fn test_get_selected_key_when_no_file() { + let storage = AccountStorage::mock().unwrap(); + + // Should return Ok(None) when no key has been selected + match storage.get_selected_key() { + Ok(None) => (), // This is what we expect + other => panic!("Expected Ok(None), got {:?}", other), + } + } +} diff --git a/crates/notedeck/src/storage/file_key_storage.rs b/crates/notedeck/src/storage/file_key_storage.rs @@ -1,163 +0,0 @@ -use crate::Result; -use enostr::{Keypair, Pubkey, SerializableKeypair}; -use tokenator::{TokenParser, TokenSerializable, TokenWriter}; - -use super::file_storage::{delete_file, write_file, Directory}; - -static SELECTED_PUBKEY_FILE_NAME: &str = "selected_pubkey"; - -/// An OS agnostic file key storage implementation -#[derive(Debug, PartialEq)] -pub struct FileKeyStorage { - keys_directory: Directory, - selected_key_directory: Directory, -} - -impl FileKeyStorage { - pub fn new(keys_directory: Directory, selected_key_directory: Directory) -> Self { - Self { - keys_directory, - selected_key_directory, - } - } - - pub fn add_key(&self, key: &Keypair) -> Result<()> { - let mut writer = TokenWriter::new("\t"); - key.serialize_tokens(&mut writer); - write_file( - &self.keys_directory.file_path, - key.pubkey.hex(), - writer.str(), - ) - } - - pub fn get_keys(&self) -> Result<Vec<Keypair>> { - let keys = self - .keys_directory - .get_files()? - .values() - .filter_map(|str_key| deserialize_kp(str_key).ok()) - .collect(); - Ok(keys) - } - - pub fn remove_key(&self, key: &Keypair) -> Result<()> { - delete_file(&self.keys_directory.file_path, key.pubkey.hex()) - } - - pub fn get_selected_key(&self) -> Result<Option<Pubkey>> { - match self - .selected_key_directory - .get_file(SELECTED_PUBKEY_FILE_NAME.to_owned()) - { - Ok(pubkey_str) => Ok(Some(serde_json::from_str(&pubkey_str)?)), - Err(crate::Error::Io(_)) => Ok(None), - Err(e) => Err(e), - } - } - - pub fn select_key(&self, pubkey: Option<Pubkey>) -> Result<()> { - if let Some(pubkey) = pubkey { - write_file( - &self.selected_key_directory.file_path, - SELECTED_PUBKEY_FILE_NAME.to_owned(), - &serde_json::to_string(&pubkey.hex())?, - ) - } else if self - .selected_key_directory - .get_file(SELECTED_PUBKEY_FILE_NAME.to_owned()) - .is_ok() - { - // Case where user chose to have no selected pubkey, but one already exists - Ok(delete_file( - &self.selected_key_directory.file_path, - SELECTED_PUBKEY_FILE_NAME.to_owned(), - )?) - } else { - Ok(()) - } - } -} - -fn deserialize_kp(serialized: &str) -> Result<Keypair> { - let data = serialized.split("\t").collect::<Vec<&str>>(); - let mut parser = TokenParser::new(&data); - - if let Ok(kp) = Keypair::parse_from_tokens(&mut parser) { - return Ok(kp); - } - - Ok(serde_json::from_str::<SerializableKeypair>(serialized)?.to_keypair("")) -} - -#[cfg(test)] -mod tests { - use std::path::PathBuf; - - use super::Result; - use super::*; - - use enostr::Keypair; - static CREATE_TMP_DIR: fn() -> Result<PathBuf> = - || Ok(tempfile::TempDir::new()?.path().to_path_buf()); - - impl FileKeyStorage { - fn mock() -> Result<Self> { - Ok(Self { - keys_directory: Directory::new(CREATE_TMP_DIR()?), - selected_key_directory: Directory::new(CREATE_TMP_DIR()?), - }) - } - } - - #[test] - fn test_basic() { - let kp = enostr::FullKeypair::generate().to_keypair(); - let storage = FileKeyStorage::mock().unwrap(); - let resp = storage.add_key(&kp); - - assert!(resp.is_ok()); - assert_num_storage(&storage.get_keys(), 1); - - assert!(storage.remove_key(&kp).is_ok()); - assert_num_storage(&storage.get_keys(), 0); - } - - fn assert_num_storage(keys_response: &Result<Vec<Keypair>>, n: usize) { - match keys_response { - Ok(keys) => { - assert_eq!(keys.len(), n); - } - Err(_e) => { - panic!("could not get keys"); - } - } - } - - #[test] - fn test_select_key() { - let kp = enostr::FullKeypair::generate().to_keypair(); - - let storage = FileKeyStorage::mock().unwrap(); - let _ = storage.add_key(&kp); - assert_num_storage(&storage.get_keys(), 1); - - let resp = storage.select_key(Some(kp.pubkey)); - assert!(resp.is_ok()); - - let resp = storage.get_selected_key(); - - assert!(resp.is_ok()); - } - - #[test] - fn test_get_selected_key_when_no_file() { - let storage = FileKeyStorage::mock().unwrap(); - - // Should return Ok(None) when no key has been selected - match storage.get_selected_key() { - Ok(None) => (), // This is what we expect - other => panic!("Expected Ok(None), got {:?}", other), - } - } -} diff --git a/crates/notedeck/src/storage/mod.rs b/crates/notedeck/src/storage/mod.rs @@ -1,5 +1,5 @@ -mod file_key_storage; +mod account_storage; mod file_storage; -pub use file_key_storage::FileKeyStorage; +pub use account_storage::AccountStorage; pub use file_storage::{delete_file, write_file, DataPath, DataPathType, Directory};