diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/client.rs | 72 | ||||
| -rw-r--r-- | src/config.rs | 62 | ||||
| -rw-r--r-- | src/key_handling/users.rs | 683 | ||||
| -rw-r--r-- | src/login.rs | 153 | ||||
| -rw-r--r-- | src/main.rs | 4 | ||||
| -rw-r--r-- | src/sub_commands/login.rs | 30 | ||||
| -rw-r--r-- | src/sub_commands/prs/create.rs | 3 |
7 files changed, 923 insertions, 84 deletions
diff --git a/src/client.rs b/src/client.rs index e0e0494..5ddf742 100644 --- a/src/client.rs +++ b/src/client.rs | |||
| @@ -10,25 +10,32 @@ | |||
| 10 | // which is currently in nightly. alternatively we can use nightly as it looks | 10 | // which is currently in nightly. alternatively we can use nightly as it looks |
| 11 | // certain that the implementation is going to make it to stable but we don't | 11 | // certain that the implementation is going to make it to stable but we don't |
| 12 | // want to inadvertlty use other features of nightly that might be removed. | 12 | // want to inadvertlty use other features of nightly that might be removed. |
| 13 | use anyhow::Result; | 13 | use anyhow::{Context, Result}; |
| 14 | use async_trait::async_trait; | 14 | use async_trait::async_trait; |
| 15 | use futures::future::join_all; | ||
| 15 | #[cfg(test)] | 16 | #[cfg(test)] |
| 16 | use mockall::*; | 17 | use mockall::*; |
| 17 | use nostr::Event; | 18 | use nostr::Event; |
| 18 | 19 | ||
| 19 | pub struct Client { | 20 | pub struct Client { |
| 20 | client: nostr_sdk::Client, | 21 | client: nostr_sdk::Client, |
| 21 | pub fallback_relays: Vec<String>, | 22 | fallback_relays: Vec<String>, |
| 22 | } | 23 | } |
| 23 | 24 | ||
| 24 | #[async_trait] | ||
| 25 | #[cfg_attr(test, automock)] | 25 | #[cfg_attr(test, automock)] |
| 26 | #[async_trait] | ||
| 26 | pub trait Connect { | 27 | pub trait Connect { |
| 27 | fn default() -> Self; | 28 | fn default() -> Self; |
| 28 | fn new(opts: Params) -> Self; | 29 | fn new(opts: Params) -> Self; |
| 29 | async fn connect(&self) -> Result<()>; | 30 | async fn connect(&self) -> Result<()>; |
| 30 | async fn disconnect(&self) -> Result<()>; | 31 | async fn disconnect(&self) -> Result<()>; |
| 32 | fn get_fallback_relays(&self) -> &Vec<String>; | ||
| 31 | async fn send_event_to(&self, url: &str, event: nostr::event::Event) -> Result<nostr::EventId>; | 33 | async fn send_event_to(&self, url: &str, event: nostr::event::Event) -> Result<nostr::EventId>; |
| 34 | async fn get_events( | ||
| 35 | &self, | ||
| 36 | relays: Vec<String>, | ||
| 37 | filters: Vec<nostr::Filter>, | ||
| 38 | ) -> Result<Vec<nostr::Event>>; | ||
| 32 | } | 39 | } |
| 33 | 40 | ||
| 34 | #[async_trait] | 41 | #[async_trait] |
| @@ -37,7 +44,7 @@ impl Connect for Client { | |||
| 37 | Client { | 44 | Client { |
| 38 | client: nostr_sdk::Client::new(&nostr::Keys::generate()), | 45 | client: nostr_sdk::Client::new(&nostr::Keys::generate()), |
| 39 | fallback_relays: vec![ | 46 | fallback_relays: vec![ |
| 40 | "ws://localhost:8080".to_string(), | 47 | "ws://localhost:8051".to_string(), |
| 41 | "ws://localhost:8052".to_string(), | 48 | "ws://localhost:8052".to_string(), |
| 42 | ], | 49 | ], |
| 43 | } | 50 | } |
| @@ -61,9 +68,52 @@ impl Connect for Client { | |||
| 61 | Ok(()) | 68 | Ok(()) |
| 62 | } | 69 | } |
| 63 | 70 | ||
| 71 | fn get_fallback_relays(&self) -> &Vec<String> { | ||
| 72 | &self.fallback_relays | ||
| 73 | } | ||
| 74 | |||
| 64 | async fn send_event_to(&self, url: &str, event: Event) -> Result<nostr::EventId> { | 75 | async fn send_event_to(&self, url: &str, event: Event) -> Result<nostr::EventId> { |
| 65 | Ok(self.client.send_event_to(url, event).await?) | 76 | Ok(self.client.send_event_to(url, event).await?) |
| 66 | } | 77 | } |
| 78 | |||
| 79 | async fn get_events( | ||
| 80 | &self, | ||
| 81 | relays: Vec<String>, | ||
| 82 | filters: Vec<nostr::Filter>, | ||
| 83 | ) -> Result<Vec<nostr::Event>> { | ||
| 84 | // add relays | ||
| 85 | for relay in &relays { | ||
| 86 | self.client | ||
| 87 | .add_relay(relay.as_str(), None) | ||
| 88 | .await | ||
| 89 | .context("cannot add relay")?; | ||
| 90 | } | ||
| 91 | |||
| 92 | let relays_map = self.client.relays().await; | ||
| 93 | |||
| 94 | let relay_results = join_all( | ||
| 95 | relays | ||
| 96 | .clone() | ||
| 97 | .iter() | ||
| 98 | .map(|r| { | ||
| 99 | ( | ||
| 100 | relays_map.get(&nostr::Url::parse(r).unwrap()).unwrap(), | ||
| 101 | filters.clone(), | ||
| 102 | ) | ||
| 103 | }) | ||
| 104 | .map(|(relay, filters)| { | ||
| 105 | relay.get_events_of( | ||
| 106 | filters, | ||
| 107 | // 20 is nostr_sdk default | ||
| 108 | std::time::Duration::from_secs(20), | ||
| 109 | nostr_sdk::FilterOptions::ExitOnEOSE, | ||
| 110 | ) | ||
| 111 | }), | ||
| 112 | ) | ||
| 113 | .await; | ||
| 114 | |||
| 115 | Ok(get_dedup_events(relay_results)) | ||
| 116 | } | ||
| 67 | } | 117 | } |
| 68 | 118 | ||
| 69 | #[derive(Default)] | 119 | #[derive(Default)] |
| @@ -82,3 +132,17 @@ impl Params { | |||
| 82 | self | 132 | self |
| 83 | } | 133 | } |
| 84 | } | 134 | } |
| 135 | |||
| 136 | fn get_dedup_events( | ||
| 137 | relay_results: Vec<Result<Vec<nostr::Event>, nostr_sdk::relay::Error>>, | ||
| 138 | ) -> Vec<Event> { | ||
| 139 | let mut dedup_events: Vec<Event> = vec![]; | ||
| 140 | for events in relay_results.into_iter().flatten() { | ||
| 141 | for event in events { | ||
| 142 | if !dedup_events.iter().any(|e| event.id.eq(&e.id)) { | ||
| 143 | dedup_events.push(event); | ||
| 144 | } | ||
| 145 | } | ||
| 146 | } | ||
| 147 | dedup_events | ||
| 148 | } | ||
diff --git a/src/config.rs b/src/config.rs index f410934..2370e34 100644 --- a/src/config.rs +++ b/src/config.rs | |||
| @@ -4,7 +4,7 @@ use anyhow::{anyhow, Context, Result}; | |||
| 4 | use directories::ProjectDirs; | 4 | use directories::ProjectDirs; |
| 5 | #[cfg(test)] | 5 | #[cfg(test)] |
| 6 | use mockall::*; | 6 | use mockall::*; |
| 7 | use nostr::secp256k1::XOnlyPublicKey; | 7 | use nostr::{secp256k1::XOnlyPublicKey, ToBech32}; |
| 8 | use serde::{self, Deserialize, Serialize}; | 8 | use serde::{self, Deserialize, Serialize}; |
| 9 | 9 | ||
| 10 | #[derive(Default)] | 10 | #[derive(Default)] |
| @@ -71,6 +71,66 @@ pub struct MyConfig { | |||
| 71 | pub struct UserRef { | 71 | pub struct UserRef { |
| 72 | pub public_key: XOnlyPublicKey, | 72 | pub public_key: XOnlyPublicKey, |
| 73 | pub encrypted_key: String, | 73 | pub encrypted_key: String, |
| 74 | pub metadata: UserMetadata, | ||
| 75 | pub relays: UserRelays, | ||
| 76 | pub last_checked: u64, | ||
| 77 | } | ||
| 78 | |||
| 79 | impl UserRef { | ||
| 80 | pub fn new(public_key: XOnlyPublicKey, encrypted_key: String) -> Self { | ||
| 81 | Self { | ||
| 82 | public_key, | ||
| 83 | encrypted_key, | ||
| 84 | relays: UserRelays { | ||
| 85 | relays: vec![], | ||
| 86 | created_at: 0, | ||
| 87 | }, | ||
| 88 | metadata: UserMetadata { | ||
| 89 | #[allow(clippy::expect_used)] | ||
| 90 | name: public_key | ||
| 91 | .to_bech32() | ||
| 92 | .expect("public key should always produce bech32"), | ||
| 93 | // name: format!( | ||
| 94 | // "{}", | ||
| 95 | // public_key | ||
| 96 | // .to_bech32() | ||
| 97 | // .expect("public key should always produce bech32"), | ||
| 98 | // ) | ||
| 99 | // .as_str()[..10].to_string(), | ||
| 100 | created_at: 0, | ||
| 101 | }, | ||
| 102 | last_checked: 0, | ||
| 103 | } | ||
| 104 | } | ||
| 105 | } | ||
| 106 | |||
| 107 | #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] | ||
| 108 | pub struct UserMetadata { | ||
| 109 | pub name: String, | ||
| 110 | pub created_at: u64, | ||
| 111 | } | ||
| 112 | |||
| 113 | #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] | ||
| 114 | pub struct UserRelays { | ||
| 115 | pub relays: Vec<UserRelayRef>, | ||
| 116 | pub created_at: u64, | ||
| 117 | } | ||
| 118 | |||
| 119 | impl UserRelays { | ||
| 120 | pub fn write(&self) -> Vec<String> { | ||
| 121 | self.relays | ||
| 122 | .iter() | ||
| 123 | .filter(|r| r.write) | ||
| 124 | .map(|r| r.url.clone()) | ||
| 125 | .collect() | ||
| 126 | } | ||
| 127 | } | ||
| 128 | |||
| 129 | #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] | ||
| 130 | pub struct UserRelayRef { | ||
| 131 | pub url: String, | ||
| 132 | pub read: bool, | ||
| 133 | pub write: bool, | ||
| 74 | } | 134 | } |
| 75 | 135 | ||
| 76 | #[cfg(test)] | 136 | #[cfg(test)] |
diff --git a/src/key_handling/users.rs b/src/key_handling/users.rs index 1d2cc34..91519bc 100644 --- a/src/key_handling/users.rs +++ b/src/key_handling/users.rs | |||
| @@ -1,11 +1,21 @@ | |||
| 1 | use std::time::SystemTime; | ||
| 2 | |||
| 1 | use anyhow::{Context, Result}; | 3 | use anyhow::{Context, Result}; |
| 4 | use async_trait::async_trait; | ||
| 2 | use nostr::prelude::*; | 5 | use nostr::prelude::*; |
| 3 | use zeroize::Zeroize; | 6 | use zeroize::Zeroize; |
| 4 | 7 | ||
| 5 | use super::encryption::{EncryptDecrypt, Encryptor}; | 8 | use super::encryption::{EncryptDecrypt, Encryptor}; |
| 9 | #[cfg(not(test))] | ||
| 10 | use crate::client::Client; | ||
| 11 | #[cfg(test)] | ||
| 12 | use crate::client::MockConnect; | ||
| 6 | use crate::{ | 13 | use crate::{ |
| 7 | cli_interactor::{Interactor, InteractorPrompt, PromptInputParms, PromptPasswordParms}, | 14 | cli_interactor::{Interactor, InteractorPrompt, PromptInputParms, PromptPasswordParms}, |
| 8 | config::{self, ConfigManagement, ConfigManager}, | 15 | client::Connect, |
| 16 | config::{ | ||
| 17 | self, ConfigManagement, ConfigManager, UserMetadata, UserRef, UserRelayRef, UserRelays, | ||
| 18 | }, | ||
| 9 | }; | 19 | }; |
| 10 | 20 | ||
| 11 | #[derive(Default)] | 21 | #[derive(Default)] |
| @@ -15,13 +25,29 @@ pub struct UserManager { | |||
| 15 | encryptor: Encryptor, | 25 | encryptor: Encryptor, |
| 16 | } | 26 | } |
| 17 | 27 | ||
| 28 | #[async_trait] | ||
| 18 | pub trait UserManagement { | 29 | pub trait UserManagement { |
| 19 | fn add(&self, nsec: &Option<String>, password: &Option<String>) -> Result<nostr::Keys>; | 30 | fn add(&self, nsec: &Option<String>, password: &Option<String>) -> Result<nostr::Keys>; |
| 31 | async fn get_user( | ||
| 32 | &self, | ||
| 33 | #[cfg(test)] client: &MockConnect, | ||
| 34 | #[cfg(not(test))] client: &Client, | ||
| 35 | public_key: &XOnlyPublicKey, | ||
| 36 | after: u64, | ||
| 37 | ) -> Result<UserRef>; | ||
| 38 | fn get_user_from_cache(&self, public_key: &XOnlyPublicKey) -> Result<UserRef>; | ||
| 39 | fn add_user_to_config( | ||
| 40 | &self, | ||
| 41 | public_key: XOnlyPublicKey, | ||
| 42 | encrypted_secret_key: Option<String>, | ||
| 43 | overwrite: bool, | ||
| 44 | ) -> Result<()>; | ||
| 20 | } | 45 | } |
| 21 | 46 | ||
| 22 | #[cfg(test)] | 47 | #[cfg(test)] |
| 23 | use duplicate::duplicate_item; | 48 | use duplicate::duplicate_item; |
| 24 | #[cfg_attr(test, duplicate_item(UserManager; [UserManager]; [self::tests::MockUserManager]))] | 49 | #[cfg_attr(test, duplicate_item(UserManager; [UserManager]; [self::tests::MockUserManager]))] |
| 50 | #[async_trait] | ||
| 25 | impl UserManagement for UserManager { | 51 | impl UserManagement for UserManager { |
| 26 | fn add(&self, nsec: &Option<String>, password: &Option<String>) -> Result<nostr::Keys> { | 52 | fn add(&self, nsec: &Option<String>, password: &Option<String>) -> Result<nostr::Keys> { |
| 27 | let mut prompt = "login with nsec (or hex private key)"; | 53 | let mut prompt = "login with nsec (or hex private key)"; |
| @@ -66,9 +92,152 @@ impl UserManagement for UserManager { | |||
| 66 | .context("failed to encrypt nsec with password.")?; | 92 | .context("failed to encrypt nsec with password.")?; |
| 67 | pass.zeroize(); | 93 | pass.zeroize(); |
| 68 | 94 | ||
| 69 | let user_ref = config::UserRef { | 95 | self.add_user_to_config(keys.public_key(), Some(encrypted_secret_key), true)?; |
| 70 | public_key: keys.public_key(), | 96 | |
| 71 | encrypted_key: encrypted_secret_key, | 97 | Ok(keys) |
| 98 | } | ||
| 99 | |||
| 100 | fn add_user_to_config( | ||
| 101 | &self, | ||
| 102 | public_key: XOnlyPublicKey, | ||
| 103 | encrypted_secret_key: Option<String>, | ||
| 104 | overwrite: bool, | ||
| 105 | ) -> Result<()> { | ||
| 106 | let user_ref = | ||
| 107 | config::UserRef::new(public_key, encrypted_secret_key.unwrap_or(String::new())); | ||
| 108 | |||
| 109 | let mut cfg = self.config_manager.load().context("failed to load application config to find and remove any old versions of the user's encrypted key")?; | ||
| 110 | // don't overwrite unless specified | ||
| 111 | if !overwrite | ||
| 112 | && cfg | ||
| 113 | .users | ||
| 114 | .clone() | ||
| 115 | .into_iter() | ||
| 116 | .any(|r| r.public_key.eq(&public_key)) | ||
| 117 | { | ||
| 118 | return Ok(()); | ||
| 119 | } | ||
| 120 | // if overwrite remove any duplicate entries for key before adding it to config | ||
| 121 | cfg.users = cfg | ||
| 122 | .users | ||
| 123 | .clone() | ||
| 124 | .into_iter() | ||
| 125 | .filter(|r| !r.public_key.eq(&public_key)) | ||
| 126 | .collect(); | ||
| 127 | cfg.users.push(user_ref); | ||
| 128 | self.config_manager | ||
| 129 | .save(&cfg) | ||
| 130 | .context("failed to save application configuration with new user details in") | ||
| 131 | } | ||
| 132 | |||
| 133 | fn get_user_from_cache(&self, public_key: &XOnlyPublicKey) -> Result<UserRef> { | ||
| 134 | let cfg = self | ||
| 135 | .config_manager | ||
| 136 | .load() | ||
| 137 | .context("failed to load application config")?; | ||
| 138 | Ok(cfg | ||
| 139 | .users | ||
| 140 | .iter() | ||
| 141 | .find(|u| u.public_key.eq(public_key)) | ||
| 142 | .context(format!("pubkey isn't a current user: {public_key}"))? | ||
| 143 | .clone()) | ||
| 144 | } | ||
| 145 | /// get UserRef fetching most recent user relays and metadata infomation | ||
| 146 | /// from relays | ||
| 147 | async fn get_user( | ||
| 148 | &self, | ||
| 149 | #[cfg(test)] client: &MockConnect, | ||
| 150 | #[cfg(not(test))] client: &Client, | ||
| 151 | public_key: &XOnlyPublicKey, | ||
| 152 | use_cache_unless_checked_more_than_x_secs_ago: u64, | ||
| 153 | ) -> Result<UserRef> { | ||
| 154 | let cfg = self | ||
| 155 | .config_manager | ||
| 156 | .load() | ||
| 157 | .context("failed to load application config")?; | ||
| 158 | let user_ref = cfg | ||
| 159 | .users | ||
| 160 | .iter() | ||
| 161 | .find(|u| u.public_key.eq(public_key)) | ||
| 162 | .context(format!("pubkey isn't a current user: {public_key}"))?; | ||
| 163 | // return cache if last fetched was within X minutes | ||
| 164 | if !unix_timestamp_after_now_plus_secs( | ||
| 165 | user_ref.last_checked, | ||
| 166 | use_cache_unless_checked_more_than_x_secs_ago, | ||
| 167 | ) { | ||
| 168 | return Ok(user_ref.clone()); | ||
| 169 | } | ||
| 170 | let events: Vec<Event> = match client | ||
| 171 | .get_events( | ||
| 172 | if user_ref.relays.write().is_empty() { | ||
| 173 | client.get_fallback_relays().clone() | ||
| 174 | } else { | ||
| 175 | user_ref.relays.write() | ||
| 176 | }, | ||
| 177 | vec![ | ||
| 178 | nostr::Filter::default() | ||
| 179 | .author(public_key.to_string()) | ||
| 180 | .since(nostr::Timestamp::from(user_ref.metadata.created_at + 1)) | ||
| 181 | .kind(Kind::Metadata), | ||
| 182 | nostr::Filter::default() | ||
| 183 | .author(public_key.to_string()) | ||
| 184 | .since(nostr::Timestamp::from(user_ref.relays.created_at + 1)) | ||
| 185 | .kind(Kind::RelayList), | ||
| 186 | ], | ||
| 187 | ) | ||
| 188 | .await | ||
| 189 | { | ||
| 190 | Ok(events) => events, | ||
| 191 | Err(_) => { | ||
| 192 | // TODO error reporting | ||
| 193 | return Ok(user_ref.clone()); | ||
| 194 | } | ||
| 195 | }; | ||
| 196 | |||
| 197 | let mut user_ref = user_ref.clone(); | ||
| 198 | |||
| 199 | user_ref.last_checked = SystemTime::now() | ||
| 200 | .duration_since(SystemTime::UNIX_EPOCH) | ||
| 201 | .context("system time should be after the year 1970")? | ||
| 202 | .as_secs(); | ||
| 203 | |||
| 204 | if let Some(new_metadata_event) = events | ||
| 205 | .iter() | ||
| 206 | .filter(|e| e.kind.eq(&nostr::Kind::Metadata) && e.pubkey.eq(public_key)) | ||
| 207 | .max_by_key(|e| e.created_at) | ||
| 208 | { | ||
| 209 | if new_metadata_event.created_at.as_u64() > user_ref.metadata.created_at { | ||
| 210 | let metadata = nostr::Metadata::from_json(new_metadata_event.content.clone()) | ||
| 211 | .context("metadata cannot be found in kind 0 event content")?; | ||
| 212 | user_ref.metadata = UserMetadata { | ||
| 213 | name: metadata | ||
| 214 | .name | ||
| 215 | .context("user metadata should always have name")?, | ||
| 216 | created_at: new_metadata_event.created_at.as_u64(), | ||
| 217 | }; | ||
| 218 | } | ||
| 219 | }; | ||
| 220 | |||
| 221 | if let Some(new_relays_event) = events | ||
| 222 | .iter() | ||
| 223 | .filter(|e| e.kind.eq(&nostr::Kind::RelayList) && e.pubkey.eq(public_key)) | ||
| 224 | .max_by_key(|e| e.created_at) | ||
| 225 | { | ||
| 226 | if new_relays_event.created_at.as_u64() > user_ref.relays.created_at { | ||
| 227 | user_ref.relays = UserRelays { | ||
| 228 | relays: new_relays_event | ||
| 229 | .tags | ||
| 230 | .iter() | ||
| 231 | .filter(|t| t.kind().eq(&nostr::TagKind::R)) | ||
| 232 | .map(|t| UserRelayRef { | ||
| 233 | url: t.as_vec()[1].clone(), | ||
| 234 | read: t.as_vec().len() == 2 || t.as_vec()[2].eq("read"), | ||
| 235 | write: t.as_vec().len() == 2 || t.as_vec()[2].eq("write"), | ||
| 236 | }) | ||
| 237 | .collect(), | ||
| 238 | created_at: new_relays_event.created_at.as_u64(), | ||
| 239 | }; | ||
| 240 | } | ||
| 72 | }; | 241 | }; |
| 73 | 242 | ||
| 74 | // remove any duplicate entries for key before adding it to config | 243 | // remove any duplicate entries for key before adding it to config |
| @@ -77,19 +246,27 @@ impl UserManagement for UserManager { | |||
| 77 | .users | 246 | .users |
| 78 | .clone() | 247 | .clone() |
| 79 | .into_iter() | 248 | .into_iter() |
| 80 | .filter(|r| !r.public_key.eq(&keys.public_key())) | 249 | .filter(|r| !r.public_key.eq(public_key)) |
| 81 | .collect(); | 250 | .collect(); |
| 82 | cfg.users.push(user_ref); | 251 | cfg.users.push(user_ref.clone()); |
| 83 | self.config_manager | 252 | self.config_manager |
| 84 | .save(&cfg) | 253 | .save(&cfg) |
| 85 | .context("failed to save application configuration with new user details in")?; | 254 | .context("failed to save application configuration with new user details in")?; |
| 255 | Ok(user_ref) | ||
| 256 | } | ||
| 257 | } | ||
| 86 | 258 | ||
| 87 | Ok(keys) | 259 | fn unix_timestamp_after_now_plus_secs(timestamp: u64, secs: u64) -> bool { |
| 260 | if let Ok(now) = SystemTime::now().duration_since(SystemTime::UNIX_EPOCH) { | ||
| 261 | now.as_secs() > (timestamp + secs) | ||
| 262 | } else { | ||
| 263 | true | ||
| 88 | } | 264 | } |
| 89 | } | 265 | } |
| 90 | 266 | ||
| 91 | #[cfg(test)] | 267 | #[cfg(test)] |
| 92 | mod tests { | 268 | mod tests { |
| 269 | use nostr; | ||
| 93 | use test_utils::*; | 270 | use test_utils::*; |
| 94 | 271 | ||
| 95 | use super::*; | 272 | use super::*; |
| @@ -278,11 +455,10 @@ mod tests { | |||
| 278 | m.config_manager = MockConfigManagement::default(); | 455 | m.config_manager = MockConfigManagement::default(); |
| 279 | m.config_manager.expect_load().returning(|| { | 456 | m.config_manager.expect_load().returning(|| { |
| 280 | Ok(MyConfig { | 457 | Ok(MyConfig { |
| 281 | users: vec![UserRef { | 458 | users: vec![UserRef::new( |
| 282 | public_key: TEST_KEY_1_KEYS.public_key(), | 459 | TEST_KEY_1_KEYS.public_key(), |
| 283 | // different key to TEST_KEY_1_ENCYPTED | 460 | TEST_KEY_2_ENCRYPTED.into(), |
| 284 | encrypted_key: TEST_KEY_2_ENCRYPTED.into(), | 461 | )], |
| 285 | }], | ||
| 286 | ..MyConfig::default() | 462 | ..MyConfig::default() |
| 287 | }) | 463 | }) |
| 288 | }); | 464 | }); |
| @@ -308,10 +484,10 @@ mod tests { | |||
| 308 | m.config_manager = MockConfigManagement::default(); | 484 | m.config_manager = MockConfigManagement::default(); |
| 309 | m.config_manager.expect_load().returning(|| { | 485 | m.config_manager.expect_load().returning(|| { |
| 310 | Ok(MyConfig { | 486 | Ok(MyConfig { |
| 311 | users: vec![UserRef { | 487 | users: vec![UserRef::new( |
| 312 | public_key: TEST_KEY_2_KEYS.public_key(), | 488 | TEST_KEY_2_KEYS.public_key(), |
| 313 | encrypted_key: TEST_KEY_2_ENCRYPTED.into(), | 489 | TEST_KEY_2_ENCRYPTED.into(), |
| 314 | }], | 490 | )], |
| 315 | ..MyConfig::default() | 491 | ..MyConfig::default() |
| 316 | }) | 492 | }) |
| 317 | }); | 493 | }); |
| @@ -329,4 +505,479 @@ mod tests { | |||
| 329 | } | 505 | } |
| 330 | } | 506 | } |
| 331 | } | 507 | } |
| 508 | |||
| 509 | fn now_timestamp() -> u64 { | ||
| 510 | SystemTime::now() | ||
| 511 | .duration_since(SystemTime::UNIX_EPOCH) | ||
| 512 | .unwrap() | ||
| 513 | .as_secs() | ||
| 514 | } | ||
| 515 | fn roughly_now(timestamp: u64) -> bool { | ||
| 516 | let now = now_timestamp(); | ||
| 517 | timestamp < now + 100 && timestamp > now - 100 | ||
| 518 | } | ||
| 519 | |||
| 520 | mod get_user { | ||
| 521 | use anyhow::anyhow; | ||
| 522 | |||
| 523 | use super::*; | ||
| 524 | use crate::client::MockConnect; | ||
| 525 | |||
| 526 | fn generate_relaylist_event() -> nostr::Event { | ||
| 527 | nostr::event::EventBuilder::new( | ||
| 528 | nostr::Kind::RelayList, | ||
| 529 | "", | ||
| 530 | &[ | ||
| 531 | nostr::Tag::RelayMetadata( | ||
| 532 | "wss://fredswrite1.relay".into(), | ||
| 533 | Some(nostr::RelayMetadata::Write), | ||
| 534 | ), | ||
| 535 | nostr::Tag::RelayMetadata( | ||
| 536 | "wss://fredsread1.relay".into(), | ||
| 537 | Some(nostr::RelayMetadata::Read), | ||
| 538 | ), | ||
| 539 | nostr::Tag::RelayMetadata("wss://fredsreadwrite.relay".into(), None), | ||
| 540 | ], | ||
| 541 | ) | ||
| 542 | .to_event(&TEST_KEY_1_KEYS) | ||
| 543 | .unwrap() | ||
| 544 | } | ||
| 545 | |||
| 546 | fn generate_relaylist_event_user_2() -> nostr::Event { | ||
| 547 | nostr::event::EventBuilder::new( | ||
| 548 | nostr::Kind::RelayList, | ||
| 549 | "", | ||
| 550 | &[ | ||
| 551 | nostr::Tag::RelayMetadata( | ||
| 552 | "wss://carolswrite1.relay".into(), | ||
| 553 | Some(nostr::RelayMetadata::Write), | ||
| 554 | ), | ||
| 555 | nostr::Tag::RelayMetadata( | ||
| 556 | "wss://carolsread1.relay".into(), | ||
| 557 | Some(nostr::RelayMetadata::Read), | ||
| 558 | ), | ||
| 559 | nostr::Tag::RelayMetadata("wss://carolsreadwrite.relay".into(), None), | ||
| 560 | ], | ||
| 561 | ) | ||
| 562 | .to_event(&TEST_KEY_2_KEYS) | ||
| 563 | .unwrap() | ||
| 564 | } | ||
| 565 | |||
| 566 | fn fallback_relays() -> Vec<String> { | ||
| 567 | vec!["ws://fallback1".to_string(), "ws://fallback2".to_string()].clone() | ||
| 568 | } | ||
| 569 | |||
| 570 | fn generate_mock_client() -> MockConnect { | ||
| 571 | let mut client = <MockConnect as std::default::Default>::default(); | ||
| 572 | client | ||
| 573 | .expect_get_fallback_relays() | ||
| 574 | .return_const(fallback_relays()); | ||
| 575 | client | ||
| 576 | } | ||
| 577 | |||
| 578 | fn generate_standard_config() -> MyConfig { | ||
| 579 | MyConfig { | ||
| 580 | users: vec![UserRef { | ||
| 581 | public_key: TEST_KEY_1_KEYS.public_key(), | ||
| 582 | encrypted_key: TEST_KEY_1_ENCRYPTED.to_string(), | ||
| 583 | metadata: UserMetadata { | ||
| 584 | name: "Fred".to_string(), | ||
| 585 | created_at: 10, | ||
| 586 | }, | ||
| 587 | relays: UserRelays { | ||
| 588 | relays: vec![ | ||
| 589 | UserRelayRef { | ||
| 590 | url: "ws://existingread".to_string(), | ||
| 591 | read: true, | ||
| 592 | write: false, | ||
| 593 | }, | ||
| 594 | UserRelayRef { | ||
| 595 | url: "ws://existingreadwrite".to_string(), | ||
| 596 | read: true, | ||
| 597 | write: true, | ||
| 598 | }, | ||
| 599 | UserRelayRef { | ||
| 600 | url: "ws://existingwrite".to_string(), | ||
| 601 | read: false, | ||
| 602 | write: true, | ||
| 603 | }, | ||
| 604 | ], | ||
| 605 | created_at: 10, | ||
| 606 | }, | ||
| 607 | last_checked: now_timestamp() - (60 * 60), // 1h ago | ||
| 608 | }], | ||
| 609 | ..MyConfig::default() | ||
| 610 | } | ||
| 611 | .clone() | ||
| 612 | } | ||
| 613 | |||
| 614 | fn expected_userrelayrefs() -> Vec<UserRelayRef> { | ||
| 615 | vec![ | ||
| 616 | UserRelayRef { | ||
| 617 | url: "wss://fredswrite1.relay".into(), | ||
| 618 | read: false, | ||
| 619 | write: true, | ||
| 620 | }, | ||
| 621 | UserRelayRef { | ||
| 622 | url: "wss://fredsread1.relay".into(), | ||
| 623 | read: true, | ||
| 624 | write: false, | ||
| 625 | }, | ||
| 626 | UserRelayRef { | ||
| 627 | url: "wss://fredsreadwrite.relay".into(), | ||
| 628 | read: true, | ||
| 629 | write: true, | ||
| 630 | }, | ||
| 631 | ] | ||
| 632 | } | ||
| 633 | |||
| 634 | mod when_within_caching_time_window { | ||
| 635 | use super::*; | ||
| 636 | |||
| 637 | #[test] | ||
| 638 | fn returns_cached_details_without_checking_relays_or_updaing_config() -> Result<()> { | ||
| 639 | let mut m = MockUserManager::default(); | ||
| 640 | let client = generate_mock_client(); | ||
| 641 | m.config_manager | ||
| 642 | .expect_load() | ||
| 643 | .returning(|| Ok(generate_standard_config())); | ||
| 644 | let res = futures::executor::block_on(m.get_user( | ||
| 645 | &client, | ||
| 646 | &TEST_KEY_1_KEYS.public_key(), | ||
| 647 | 24 * 60 * 60, // within 24 hours | ||
| 648 | ))?; | ||
| 649 | assert_eq!(res.metadata.name, "Fred"); | ||
| 650 | assert_eq!(res.relays.relays[0].url, "ws://existingread"); | ||
| 651 | Ok(()) | ||
| 652 | } | ||
| 653 | } | ||
| 654 | |||
| 655 | mod returns_userref_with_latest_details_from_events_on_relays { | ||
| 656 | use super::*; | ||
| 657 | |||
| 658 | #[test] | ||
| 659 | fn name() -> Result<()> { | ||
| 660 | let mut m = MockUserManager::default(); | ||
| 661 | let mut client = generate_mock_client(); | ||
| 662 | m.config_manager | ||
| 663 | .expect_load() | ||
| 664 | .returning(|| Ok(generate_standard_config())); | ||
| 665 | m.config_manager.expect_save().returning(|_| Ok(())); | ||
| 666 | client | ||
| 667 | .expect_get_events() | ||
| 668 | .returning(|_, _| Ok(vec![generate_test_key_1_metadata_event("fred")])); | ||
| 669 | |||
| 670 | let res = futures::executor::block_on(m.get_user( | ||
| 671 | &client, | ||
| 672 | &TEST_KEY_1_KEYS.public_key(), | ||
| 673 | 5 * 60, // 5 mins ago | ||
| 674 | ))?; | ||
| 675 | assert_eq!(res.metadata.name, "fred"); | ||
| 676 | Ok(()) | ||
| 677 | } | ||
| 678 | |||
| 679 | #[test] | ||
| 680 | fn name_ignoring_other_users_events() -> Result<()> { | ||
| 681 | let mut m = MockUserManager::default(); | ||
| 682 | let mut client = generate_mock_client(); | ||
| 683 | m.config_manager | ||
| 684 | .expect_load() | ||
| 685 | .returning(|| Ok(generate_standard_config())); | ||
| 686 | m.config_manager.expect_save().returning(|_| Ok(())); | ||
| 687 | client.expect_get_events().returning(|_, _| { | ||
| 688 | Ok(vec![ | ||
| 689 | generate_test_key_2_metadata_event("carole"), | ||
| 690 | generate_test_key_1_metadata_event_old("fred"), | ||
| 691 | ]) | ||
| 692 | }); | ||
| 693 | |||
| 694 | let res = futures::executor::block_on(m.get_user( | ||
| 695 | &client, | ||
| 696 | &TEST_KEY_1_KEYS.public_key(), | ||
| 697 | 5 * 60, // 5 mins ago | ||
| 698 | ))?; | ||
| 699 | assert_eq!(res.metadata.name, "fred"); | ||
| 700 | Ok(()) | ||
| 701 | } | ||
| 702 | |||
| 703 | #[test] | ||
| 704 | fn relays() -> Result<()> { | ||
| 705 | let mut m = MockUserManager::default(); | ||
| 706 | let mut client = generate_mock_client(); | ||
| 707 | m.config_manager | ||
| 708 | .expect_load() | ||
| 709 | .returning(|| Ok(generate_standard_config())); | ||
| 710 | m.config_manager.expect_save().returning(|_| Ok(())); | ||
| 711 | client.expect_get_events().returning(|_, _| { | ||
| 712 | Ok(vec![ | ||
| 713 | generate_test_key_1_metadata_event("fred"), | ||
| 714 | generate_relaylist_event(), | ||
| 715 | ]) | ||
| 716 | }); | ||
| 717 | |||
| 718 | let res = futures::executor::block_on(m.get_user( | ||
| 719 | &client, | ||
| 720 | &TEST_KEY_1_KEYS.public_key(), | ||
| 721 | 5 * 60, // 5 mins ago | ||
| 722 | ))?; | ||
| 723 | assert_eq!(res.relays.relays, expected_userrelayrefs(),); | ||
| 724 | Ok(()) | ||
| 725 | } | ||
| 726 | |||
| 727 | #[test] | ||
| 728 | fn relays_ignoring_other_users_events() -> Result<()> { | ||
| 729 | let mut m = MockUserManager::default(); | ||
| 730 | let mut client = generate_mock_client(); | ||
| 731 | m.config_manager | ||
| 732 | .expect_load() | ||
| 733 | .returning(|| Ok(generate_standard_config())); | ||
| 734 | m.config_manager.expect_save().returning(|_| Ok(())); | ||
| 735 | client.expect_get_events().returning(|_, _| { | ||
| 736 | Ok(vec![ | ||
| 737 | make_event_old_or_change_user( | ||
| 738 | generate_relaylist_event(), | ||
| 739 | &TEST_KEY_1_KEYS, | ||
| 740 | 10000, | ||
| 741 | ), | ||
| 742 | generate_relaylist_event_user_2(), | ||
| 743 | ]) | ||
| 744 | }); | ||
| 745 | |||
| 746 | let res = futures::executor::block_on(m.get_user( | ||
| 747 | &client, | ||
| 748 | &TEST_KEY_1_KEYS.public_key(), | ||
| 749 | 5 * 60, // 5 mins ago | ||
| 750 | ))?; | ||
| 751 | assert_eq!(res.relays.relays, expected_userrelayrefs(),); | ||
| 752 | Ok(()) | ||
| 753 | } | ||
| 754 | } | ||
| 755 | |||
| 756 | mod saves_updates_to_config { | ||
| 757 | use super::*; | ||
| 758 | |||
| 759 | #[test] | ||
| 760 | fn saves_name_to_config() -> Result<()> { | ||
| 761 | let mut m = MockUserManager::default(); | ||
| 762 | let mut client = generate_mock_client(); | ||
| 763 | m.config_manager | ||
| 764 | .expect_load() | ||
| 765 | .returning(|| Ok(generate_standard_config())); | ||
| 766 | m.config_manager | ||
| 767 | .expect_save() | ||
| 768 | .once() | ||
| 769 | .withf(|cfg| cfg.users[0].metadata.name.eq("fred")) | ||
| 770 | .returning(|_| Ok(())); | ||
| 771 | client | ||
| 772 | .expect_get_events() | ||
| 773 | .returning(|_, _| Ok(vec![generate_test_key_1_metadata_event("fred")])); | ||
| 774 | |||
| 775 | futures::executor::block_on(m.get_user( | ||
| 776 | &client, | ||
| 777 | &TEST_KEY_1_KEYS.public_key(), | ||
| 778 | 5 * 60, // 5 mins ago | ||
| 779 | ))?; | ||
| 780 | Ok(()) | ||
| 781 | } | ||
| 782 | |||
| 783 | #[test] | ||
| 784 | fn updates_metadata_created_at() -> Result<()> { | ||
| 785 | let mut m = MockUserManager::default(); | ||
| 786 | let mut client = generate_mock_client(); | ||
| 787 | m.config_manager | ||
| 788 | .expect_load() | ||
| 789 | .returning(|| Ok(generate_standard_config())); | ||
| 790 | m.config_manager | ||
| 791 | .expect_save() | ||
| 792 | .once() | ||
| 793 | .withf(|cfg| roughly_now(cfg.users[0].metadata.created_at)) | ||
| 794 | .returning(|_| Ok(())); | ||
| 795 | client | ||
| 796 | .expect_get_events() | ||
| 797 | .returning(|_, _| Ok(vec![generate_test_key_1_metadata_event("fred")])); | ||
| 798 | |||
| 799 | futures::executor::block_on(m.get_user( | ||
| 800 | &client, | ||
| 801 | &TEST_KEY_1_KEYS.public_key(), | ||
| 802 | 5 * 60, // 5 mins ago | ||
| 803 | ))?; | ||
| 804 | Ok(()) | ||
| 805 | } | ||
| 806 | |||
| 807 | #[test] | ||
| 808 | fn saves_relays_to_config() -> Result<()> { | ||
| 809 | let mut m = MockUserManager::default(); | ||
| 810 | let mut client = generate_mock_client(); | ||
| 811 | m.config_manager | ||
| 812 | .expect_load() | ||
| 813 | .returning(|| Ok(generate_standard_config())); | ||
| 814 | m.config_manager | ||
| 815 | .expect_save() | ||
| 816 | .once() | ||
| 817 | .withf(|cfg| expected_userrelayrefs().eq(&cfg.users[0].relays.relays)) | ||
| 818 | .returning(|_| Ok(())); | ||
| 819 | client | ||
| 820 | .expect_get_events() | ||
| 821 | .returning(|_, _| Ok(vec![generate_relaylist_event()])); | ||
| 822 | |||
| 823 | futures::executor::block_on(m.get_user( | ||
| 824 | &client, | ||
| 825 | &TEST_KEY_1_KEYS.public_key(), | ||
| 826 | 5 * 60, // 5 mins ago | ||
| 827 | ))?; | ||
| 828 | Ok(()) | ||
| 829 | } | ||
| 830 | |||
| 831 | #[test] | ||
| 832 | fn updates_relays_created_at() -> Result<()> { | ||
| 833 | let mut m = MockUserManager::default(); | ||
| 834 | let mut client = generate_mock_client(); | ||
| 835 | m.config_manager | ||
| 836 | .expect_load() | ||
| 837 | .returning(|| Ok(generate_standard_config())); | ||
| 838 | m.config_manager | ||
| 839 | .expect_save() | ||
| 840 | .once() | ||
| 841 | .withf(|cfg| roughly_now(cfg.users[0].relays.created_at)) | ||
| 842 | .returning(|_| Ok(())); | ||
| 843 | client | ||
| 844 | .expect_get_events() | ||
| 845 | .returning(|_, _| Ok(vec![generate_relaylist_event()])); | ||
| 846 | |||
| 847 | futures::executor::block_on(m.get_user( | ||
| 848 | &client, | ||
| 849 | &TEST_KEY_1_KEYS.public_key(), | ||
| 850 | 5 * 60, // 5 mins ago | ||
| 851 | ))?; | ||
| 852 | Ok(()) | ||
| 853 | } | ||
| 854 | |||
| 855 | #[test] | ||
| 856 | fn when_no_changes_updates_last_updated() -> Result<()> { | ||
| 857 | let mut m = MockUserManager::default(); | ||
| 858 | let mut client = generate_mock_client(); | ||
| 859 | m.config_manager | ||
| 860 | .expect_load() | ||
| 861 | .returning(|| Ok(generate_standard_config())); | ||
| 862 | m.config_manager | ||
| 863 | .expect_save() | ||
| 864 | .once() | ||
| 865 | .withf(|cfg| roughly_now(cfg.users[0].last_checked)) | ||
| 866 | .returning(|_| Ok(())); | ||
| 867 | client.expect_get_events().returning(|_, _| Ok(vec![])); | ||
| 868 | |||
| 869 | futures::executor::block_on(m.get_user( | ||
| 870 | &client, | ||
| 871 | &TEST_KEY_1_KEYS.public_key(), | ||
| 872 | 5 * 60, // 5 mins ago | ||
| 873 | ))?; | ||
| 874 | Ok(()) | ||
| 875 | } | ||
| 876 | |||
| 877 | #[test] | ||
| 878 | fn when_changes_updates_last_updated() -> Result<()> { | ||
| 879 | let mut m = MockUserManager::default(); | ||
| 880 | let mut client = generate_mock_client(); | ||
| 881 | m.config_manager | ||
| 882 | .expect_load() | ||
| 883 | .returning(|| Ok(generate_standard_config())); | ||
| 884 | m.config_manager | ||
| 885 | .expect_save() | ||
| 886 | .once() | ||
| 887 | .withf(|cfg| roughly_now(cfg.users[0].last_checked)) | ||
| 888 | .returning(|_| Ok(())); | ||
| 889 | client | ||
| 890 | .expect_get_events() | ||
| 891 | .returning(|_, _| Ok(vec![generate_test_key_1_metadata_event("fred")])); | ||
| 892 | |||
| 893 | futures::executor::block_on(m.get_user( | ||
| 894 | &client, | ||
| 895 | &TEST_KEY_1_KEYS.public_key(), | ||
| 896 | 5 * 60, // 5 mins ago | ||
| 897 | ))?; | ||
| 898 | Ok(()) | ||
| 899 | } | ||
| 900 | } | ||
| 901 | |||
| 902 | mod fetches_from_correct_relays { | ||
| 903 | use super::*; | ||
| 904 | #[test] | ||
| 905 | fn when_userref_write_relays_present_fetches_only_from_them() -> Result<()> { | ||
| 906 | let mut m = MockUserManager::default(); | ||
| 907 | let mut client = generate_mock_client(); | ||
| 908 | m.config_manager | ||
| 909 | .expect_load() | ||
| 910 | .returning(|| Ok(generate_standard_config())); | ||
| 911 | m.config_manager.expect_save().returning(|_| Ok(())); | ||
| 912 | client | ||
| 913 | .expect_get_events() | ||
| 914 | .once() | ||
| 915 | .withf(move |relays, _filters| { | ||
| 916 | vec![ | ||
| 917 | "ws://existingreadwrite".to_string(), | ||
| 918 | "ws://existingwrite".to_string(), | ||
| 919 | ] | ||
| 920 | .eq(relays) | ||
| 921 | }) | ||
| 922 | .returning(|_, _| Ok(vec![])); | ||
| 923 | |||
| 924 | futures::executor::block_on(m.get_user( | ||
| 925 | &client, | ||
| 926 | &TEST_KEY_1_KEYS.public_key(), | ||
| 927 | 5 * 60, // 5 mins ago | ||
| 928 | ))?; | ||
| 929 | Ok(()) | ||
| 930 | } | ||
| 931 | #[test] | ||
| 932 | fn when_userref_write_relays_not_present_fetches_from_fallback_relays() -> Result<()> { | ||
| 933 | let mut m = MockUserManager::default(); | ||
| 934 | let mut client = generate_mock_client(); | ||
| 935 | m.config_manager.expect_load().returning(|| { | ||
| 936 | Ok(MyConfig { | ||
| 937 | users: vec![UserRef { | ||
| 938 | relays: UserRelays { | ||
| 939 | relays: vec![], | ||
| 940 | created_at: 0, | ||
| 941 | }, | ||
| 942 | ..generate_standard_config().users[0].clone() | ||
| 943 | }], | ||
| 944 | ..generate_standard_config() | ||
| 945 | }) | ||
| 946 | }); | ||
| 947 | m.config_manager.expect_save().returning(|_| Ok(())); | ||
| 948 | client | ||
| 949 | .expect_get_events() | ||
| 950 | .once() | ||
| 951 | .withf(move |relays, _filters| fallback_relays().eq(relays)) | ||
| 952 | .returning(|_, _| Ok(vec![])); | ||
| 953 | |||
| 954 | futures::executor::block_on(m.get_user( | ||
| 955 | &client, | ||
| 956 | &TEST_KEY_1_KEYS.public_key(), | ||
| 957 | 5 * 60, // 5 mins ago | ||
| 958 | ))?; | ||
| 959 | Ok(()) | ||
| 960 | } | ||
| 961 | } | ||
| 962 | |||
| 963 | #[test] | ||
| 964 | fn when_failed_to_fetch_events_returns_cached_details() -> Result<()> { | ||
| 965 | let mut m = MockUserManager::default(); | ||
| 966 | let mut client = generate_mock_client(); | ||
| 967 | m.config_manager | ||
| 968 | .expect_load() | ||
| 969 | .returning(|| Ok(generate_standard_config())); | ||
| 970 | client | ||
| 971 | .expect_get_events() | ||
| 972 | .returning(|_, _| Err(anyhow!("test error"))); | ||
| 973 | |||
| 974 | let res = futures::executor::block_on(m.get_user( | ||
| 975 | &client, | ||
| 976 | &TEST_KEY_1_KEYS.public_key(), | ||
| 977 | 5 * 60, // 10 mins ago | ||
| 978 | ))?; | ||
| 979 | assert_eq!(res.metadata.name, "Fred"); | ||
| 980 | Ok(()) | ||
| 981 | } | ||
| 982 | } | ||
| 332 | } | 983 | } |
diff --git a/src/login.rs b/src/login.rs index 12fe76e..e73373a 100644 --- a/src/login.rs +++ b/src/login.rs | |||
| @@ -1,10 +1,14 @@ | |||
| 1 | use anyhow::{bail, Context, Result}; | 1 | use anyhow::{bail, Context, Result}; |
| 2 | use nostr::prelude::{FromSkStr, ToBech32}; | 2 | use nostr::{prelude::FromSkStr, secp256k1::XOnlyPublicKey}; |
| 3 | use zeroize::Zeroize; | 3 | use zeroize::Zeroize; |
| 4 | 4 | ||
| 5 | #[cfg(not(test))] | ||
| 6 | use crate::client::Client; | ||
| 7 | #[cfg(test)] | ||
| 8 | use crate::client::MockConnect; | ||
| 5 | use crate::{ | 9 | use crate::{ |
| 6 | cli_interactor::{Interactor, InteractorPrompt, PromptPasswordParms}, | 10 | cli_interactor::{Interactor, InteractorPrompt, PromptPasswordParms}, |
| 7 | config::{ConfigManagement, ConfigManager}, | 11 | config::{ConfigManagement, ConfigManager, UserRef}, |
| 8 | key_handling::{ | 12 | key_handling::{ |
| 9 | encryption::{EncryptDecrypt, Encryptor}, | 13 | encryption::{EncryptDecrypt, Encryptor}, |
| 10 | users::{UserManagement, UserManager}, | 14 | users::{UserManagement, UserManager}, |
| @@ -12,77 +16,114 @@ use crate::{ | |||
| 12 | }; | 16 | }; |
| 13 | 17 | ||
| 14 | /// handles the encrpytion and storage of key material | 18 | /// handles the encrpytion and storage of key material |
| 15 | pub fn launch(nsec: &Option<String>, password: &Option<String>) -> Result<nostr::Keys> { | 19 | pub async fn launch( |
| 20 | nsec: &Option<String>, | ||
| 21 | password: &Option<String>, | ||
| 22 | #[cfg(test)] client: Option<&MockConnect>, | ||
| 23 | #[cfg(not(test))] client: Option<&Client>, | ||
| 24 | ) -> Result<nostr::Keys> { | ||
| 16 | // if nsec parameter | 25 | // if nsec parameter |
| 17 | if let Some(nsec_unwrapped) = nsec { | 26 | let key = if let Some(nsec_unwrapped) = nsec { |
| 18 | // get key or fail without prompts | 27 | // get key or fail without prompts |
| 19 | let key = nostr::Keys::from_sk_str(nsec_unwrapped).context("invalid nsec parameter")?; | 28 | let key = nostr::Keys::from_sk_str(nsec_unwrapped).context("invalid nsec parameter")?; |
| 20 | println!( | ||
| 21 | "logged in as {}", | ||
| 22 | &key.public_key() | ||
| 23 | .to_bech32() | ||
| 24 | .context("public key should always produce bech32")? | ||
| 25 | ); | ||
| 26 | 29 | ||
| 27 | // if password, add user to enable password login in future | 30 | // if password, add user to enable password login in future |
| 28 | if password.is_some() { | 31 | if password.is_some() { |
| 29 | UserManager::default() | 32 | UserManager::default() |
| 30 | .add(nsec, password) | 33 | .add(nsec, password) |
| 31 | .context("could not store identity")?; | 34 | .context("could not store identity")?; |
| 35 | } else { | ||
| 36 | UserManager::default().add_user_to_config(key.public_key(), None, false)?; | ||
| 32 | } | 37 | } |
| 33 | return Ok(key); | 38 | key |
| 34 | } | 39 | } else { |
| 40 | let cfg = ConfigManager | ||
| 41 | .load() | ||
| 42 | .context("failed to load application config")?; | ||
| 43 | // if encrypted nsec present | ||
| 44 | if cfg.users.last().is_some() && !cfg.users.last().unwrap().encrypted_key.is_empty() { | ||
| 45 | // unfortunately this line is unstable in rust: | ||
| 46 | // if let Some(user) = cfg.users.last() && !user.encrypted_key.is_empty() { | ||
| 47 | let user = cfg.users.last().unwrap(); | ||
| 48 | let mut pass = if let Some(p) = password.clone() { | ||
| 49 | p | ||
| 50 | } else { | ||
| 51 | println!("login as {}", &user.metadata.name); | ||
| 52 | Interactor::default() | ||
| 53 | .password(PromptPasswordParms::default().with_prompt("password")) | ||
| 54 | .context("failed to get password input from interactor.password")? | ||
| 55 | }; | ||
| 35 | 56 | ||
| 36 | // if encrypted nsec stored, attempt password | 57 | let key_result = Encryptor |
| 37 | let cfg = ConfigManager | 58 | .decrypt_key(&user.encrypted_key, pass.as_str()) |
| 38 | .load() | 59 | .context("failed to decrypt key with provided password"); |
| 39 | .context("failed to load application config")?; | 60 | pass.zeroize(); |
| 40 | let key = if let Some(user) = cfg.users.last() { | ||
| 41 | let mut pass = if let Some(p) = password.clone() { | ||
| 42 | p | ||
| 43 | } else { | ||
| 44 | println!( | ||
| 45 | "login as {}", | ||
| 46 | &user | ||
| 47 | .public_key | ||
| 48 | .to_bech32() | ||
| 49 | .context("public key should always produce bech32")? | ||
| 50 | ); | ||
| 51 | Interactor::default() | ||
| 52 | .password(PromptPasswordParms::default().with_prompt("password")) | ||
| 53 | .context("failed to get password input from interactor.password")? | ||
| 54 | }; | ||
| 55 | 61 | ||
| 56 | let key_result = Encryptor | 62 | key_result.context(format!("failed to log in as {}", &user.metadata.name))? |
| 57 | .decrypt_key(&user.encrypted_key, pass.as_str()) | 63 | } |
| 58 | .context("failed to decrypt key with provided password"); | 64 | // no encrypted nsec present |
| 59 | pass.zeroize(); | 65 | else { |
| 66 | // no nsec but password supplied | ||
| 67 | if password.is_some() { | ||
| 68 | bail!("no nsec available to decrypt with specified password"); | ||
| 69 | } | ||
| 70 | // otherwise add new user with nsec and password prompts | ||
| 71 | UserManager::default() | ||
| 72 | .add(nsec, password) | ||
| 73 | .context("failed to add user")? | ||
| 74 | } | ||
| 75 | }; | ||
| 60 | 76 | ||
| 61 | key_result.context(format!( | 77 | // get user details |
| 62 | "failed to log in as {}", | 78 | let user_ref = if let Some(client) = client { |
| 63 | &user | 79 | get_user_details(&key.public_key(), client).await? |
| 64 | .public_key | ||
| 65 | .to_bech32() | ||
| 66 | .context("public key should always produce bech32")? | ||
| 67 | ))? | ||
| 68 | } else { | 80 | } else { |
| 69 | // no nsec but password supplied | 81 | // this will get user details with name as npub |
| 70 | if password.is_some() { | ||
| 71 | bail!("no nsec available to decrypt with specified password"); | ||
| 72 | } | ||
| 73 | // otherwise add new user with nsec and password prompts | ||
| 74 | UserManager::default() | 82 | UserManager::default() |
| 75 | .add(nsec, password) | 83 | .get_user_from_cache(&key.public_key())? |
| 76 | .context("failed to add user")? | 84 | .clone() |
| 77 | }; | 85 | }; |
| 78 | println!( | ||
| 79 | "logged in as {}", | ||
| 80 | &key.public_key() | ||
| 81 | .to_bech32() | ||
| 82 | .context("public key should always produce bech32")? | ||
| 83 | ); | ||
| 84 | 86 | ||
| 85 | // fetching metdata | 87 | // print logged in |
| 88 | println!("logged in as {}", user_ref.metadata.name); | ||
| 86 | 89 | ||
| 87 | Ok(key) | 90 | Ok(key) |
| 88 | } | 91 | } |
| 92 | |||
| 93 | async fn get_user_details( | ||
| 94 | public_key: &XOnlyPublicKey, | ||
| 95 | #[cfg(test)] client: &crate::client::MockConnect, | ||
| 96 | #[cfg(not(test))] client: &Client, | ||
| 97 | ) -> Result<UserRef> { | ||
| 98 | let term = console::Term::stdout(); | ||
| 99 | term.write_line("searching for your details...")?; | ||
| 100 | let user_manager = UserManager::default(); | ||
| 101 | let user_ref = user_manager | ||
| 102 | .get_user( | ||
| 103 | client, | ||
| 104 | public_key, | ||
| 105 | // 1 hour | ||
| 106 | 60 * 60, | ||
| 107 | ) | ||
| 108 | .await?; | ||
| 109 | term.clear_last_lines(1)?; | ||
| 110 | if user_ref.metadata.created_at.eq(&0) { | ||
| 111 | println!("cannot find your account metadata (name, etc) on relays",); | ||
| 112 | // TODO use secondary fallback list of relays. | ||
| 113 | // TODO better reporting of what relays were checked and what the user | ||
| 114 | // here is a starter: | ||
| 115 | // cannot find account details on relays: | ||
| 116 | // - purplepages.xyz | ||
| 117 | // - fallbackrelay1 | ||
| 118 | // - ... | ||
| 119 | // would you like to: | ||
| 120 | // [-] proceed anyway | ||
| 121 | // - add custom fallback relays | ||
| 122 | } else if user_ref.relays.created_at.eq(&0) { | ||
| 123 | println!( | ||
| 124 | "cannot find your relay list. consider using another nostr client to create one to enhance your nostr experience." | ||
| 125 | ); | ||
| 126 | // TODO better guidance on how to do this | ||
| 127 | } | ||
| 128 | Ok(user_ref) | ||
| 129 | } | ||
diff --git a/src/main.rs b/src/main.rs index 5c8518b..68b0ed6 100644 --- a/src/main.rs +++ b/src/main.rs | |||
| @@ -41,7 +41,9 @@ enum Commands { | |||
| 41 | async fn main() -> Result<()> { | 41 | async fn main() -> Result<()> { |
| 42 | let cli = Cli::parse(); | 42 | let cli = Cli::parse(); |
| 43 | match &cli.command { | 43 | match &cli.command { |
| 44 | Commands::Login(args) => sub_commands::login::launch(&cli, args), | 44 | Commands::Login(args) => { |
| 45 | futures::executor::block_on(sub_commands::login::launch(&cli, args)) | ||
| 46 | } | ||
| 45 | Commands::Prs(args) => futures::executor::block_on(sub_commands::prs::launch(&cli, args)), | 47 | Commands::Prs(args) => futures::executor::block_on(sub_commands::prs::launch(&cli, args)), |
| 46 | } | 48 | } |
| 47 | } | 49 | } |
diff --git a/src/sub_commands/login.rs b/src/sub_commands/login.rs index 5391024..b93e9bc 100644 --- a/src/sub_commands/login.rs +++ b/src/sub_commands/login.rs | |||
| @@ -1,12 +1,32 @@ | |||
| 1 | use anyhow::Result; | 1 | use anyhow::Result; |
| 2 | use clap; | 2 | use clap; |
| 3 | 3 | ||
| 4 | use crate::{login, Cli}; | 4 | #[cfg(not(test))] |
| 5 | use crate::client::Client; | ||
| 6 | #[cfg(test)] | ||
| 7 | use crate::client::MockConnect; | ||
| 8 | use crate::{client::Connect, login, Cli}; | ||
| 5 | 9 | ||
| 6 | #[derive(clap::Args)] | 10 | #[derive(clap::Args)] |
| 7 | pub struct SubCommandArgs; | 11 | pub struct SubCommandArgs { |
| 12 | /// don't fetch user metadata and relay list from relays | ||
| 13 | #[arg(long, action)] | ||
| 14 | offline: bool, | ||
| 15 | } | ||
| 16 | |||
| 17 | pub async fn launch(args: &Cli, command_args: &SubCommandArgs) -> Result<()> { | ||
| 18 | if command_args.offline { | ||
| 19 | login::launch(&args.nsec, &args.password, None).await?; | ||
| 20 | Ok(()) | ||
| 21 | } else { | ||
| 22 | #[cfg(not(test))] | ||
| 23 | let client = Client::default(); | ||
| 24 | #[cfg(test)] | ||
| 25 | let client = <MockConnect as std::default::Default>::default(); | ||
| 8 | 26 | ||
| 9 | pub fn launch(args: &Cli, _command_args: &SubCommandArgs) -> Result<()> { | 27 | client.connect().await?; |
| 10 | let _ = login::launch(&args.nsec, &args.password)?; | 28 | login::launch(&args.nsec, &args.password, Some(&client)).await?; |
| 11 | Ok(()) | 29 | client.disconnect().await?; |
| 30 | Ok(()) | ||
| 31 | } | ||
| 12 | } | 32 | } |
diff --git a/src/sub_commands/prs/create.rs b/src/sub_commands/prs/create.rs index 3047e92..0249488 100644 --- a/src/sub_commands/prs/create.rs +++ b/src/sub_commands/prs/create.rs | |||
| @@ -86,7 +86,8 @@ pub async fn launch( | |||
| 86 | 86 | ||
| 87 | // create PR event | 87 | // create PR event |
| 88 | 88 | ||
| 89 | let keys = login::launch(&cli_args.nsec, &cli_args.password)?; | 89 | // TODO add client here |
| 90 | let keys = login::launch(&cli_args.nsec, &cli_args.password, None).await?; | ||
| 90 | 91 | ||
| 91 | let events = | 92 | let events = |
| 92 | generate_pr_and_patch_events(&title, &description, &to_branch, &git_repo, &ahead, keys)?; | 93 | generate_pr_and_patch_events(&title, &description, &to_branch, &git_repo, &ahead, keys)?; |