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:
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)?;