diff options
| author | DanConwayDev <DanConwayDev@protonmail.com> | 2023-10-01 00:00:00 +0100 |
|---|---|---|
| committer | DanConwayDev <DanConwayDev@protonmail.com> | 2023-10-01 00:00:00 +0100 |
| commit | e237328ec611a5891586530c1d3cb26c16c1093b (patch) | |
| tree | 22ac36baa240354d06ae82eb070609fa3e3fcb82 /src | |
| parent | 000901c0cbca8464b5a89bcc93c5474f6564bafd (diff) | |
feat(login) fetch user relays and metadata
get user relay list and metadata events from relays when keys are
used and last fetch attempt was more than an hour ago
uses user's write relays if known, otherwise uses fallback relays
to achieve this a method for intergration testing event fetching
from relays was added
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)?; |