upleb.uk

Public git repos — served from a NIP-34 GRASP relay at git.upleb.uk

summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorDanConwayDev <DanConwayDev@protonmail.com>2023-10-01 00:00:00 +0100
committerDanConwayDev <DanConwayDev@protonmail.com>2023-10-01 00:00:00 +0100
commite237328ec611a5891586530c1d3cb26c16c1093b (patch)
tree22ac36baa240354d06ae82eb070609fa3e3fcb82 /src
parent000901c0cbca8464b5a89bcc93c5474f6564bafd (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.rs72
-rw-r--r--src/config.rs62
-rw-r--r--src/key_handling/users.rs683
-rw-r--r--src/login.rs153
-rw-r--r--src/main.rs4
-rw-r--r--src/sub_commands/login.rs30
-rw-r--r--src/sub_commands/prs/create.rs3
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.
13use anyhow::Result; 13use anyhow::{Context, Result};
14use async_trait::async_trait; 14use async_trait::async_trait;
15use futures::future::join_all;
15#[cfg(test)] 16#[cfg(test)]
16use mockall::*; 17use mockall::*;
17use nostr::Event; 18use nostr::Event;
18 19
19pub struct Client { 20pub 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]
26pub trait Connect { 27pub 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
136fn 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};
4use directories::ProjectDirs; 4use directories::ProjectDirs;
5#[cfg(test)] 5#[cfg(test)]
6use mockall::*; 6use mockall::*;
7use nostr::secp256k1::XOnlyPublicKey; 7use nostr::{secp256k1::XOnlyPublicKey, ToBech32};
8use serde::{self, Deserialize, Serialize}; 8use serde::{self, Deserialize, Serialize};
9 9
10#[derive(Default)] 10#[derive(Default)]
@@ -71,6 +71,66 @@ pub struct MyConfig {
71pub struct UserRef { 71pub 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
79impl 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)]
108pub struct UserMetadata {
109 pub name: String,
110 pub created_at: u64,
111}
112
113#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
114pub struct UserRelays {
115 pub relays: Vec<UserRelayRef>,
116 pub created_at: u64,
117}
118
119impl 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)]
130pub 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 @@
1use std::time::SystemTime;
2
1use anyhow::{Context, Result}; 3use anyhow::{Context, Result};
4use async_trait::async_trait;
2use nostr::prelude::*; 5use nostr::prelude::*;
3use zeroize::Zeroize; 6use zeroize::Zeroize;
4 7
5use super::encryption::{EncryptDecrypt, Encryptor}; 8use super::encryption::{EncryptDecrypt, Encryptor};
9#[cfg(not(test))]
10use crate::client::Client;
11#[cfg(test)]
12use crate::client::MockConnect;
6use crate::{ 13use 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]
18pub trait UserManagement { 29pub 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)]
23use duplicate::duplicate_item; 48use 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]
25impl UserManagement for UserManager { 51impl 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) 259fn 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)]
92mod tests { 268mod 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 @@
1use anyhow::{bail, Context, Result}; 1use anyhow::{bail, Context, Result};
2use nostr::prelude::{FromSkStr, ToBech32}; 2use nostr::{prelude::FromSkStr, secp256k1::XOnlyPublicKey};
3use zeroize::Zeroize; 3use zeroize::Zeroize;
4 4
5#[cfg(not(test))]
6use crate::client::Client;
7#[cfg(test)]
8use crate::client::MockConnect;
5use crate::{ 9use 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
15pub fn launch(nsec: &Option<String>, password: &Option<String>) -> Result<nostr::Keys> { 19pub 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
93async 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 {
41async fn main() -> Result<()> { 41async 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 @@
1use anyhow::Result; 1use anyhow::Result;
2use clap; 2use clap;
3 3
4use crate::{login, Cli}; 4#[cfg(not(test))]
5use crate::client::Client;
6#[cfg(test)]
7use crate::client::MockConnect;
8use crate::{client::Connect, login, Cli};
5 9
6#[derive(clap::Args)] 10#[derive(clap::Args)]
7pub struct SubCommandArgs; 11pub struct SubCommandArgs {
12 /// don't fetch user metadata and relay list from relays
13 #[arg(long, action)]
14 offline: bool,
15}
16
17pub 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
9pub 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)?;