upleb.uk

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

summaryrefslogtreecommitdiff
path: root/src/lib/login/mod.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/lib/login/mod.rs')
-rw-r--r--src/lib/login/mod.rs883
1 files changed, 56 insertions, 827 deletions
diff --git a/src/lib/login/mod.rs b/src/lib/login/mod.rs
index 0e00170..b45bc1d 100644
--- a/src/lib/login/mod.rs
+++ b/src/lib/login/mod.rs
@@ -1,129 +1,65 @@
1use std::{collections::HashSet, path::Path, str::FromStr, sync::Arc, time::Duration}; 1use std::{path::Path, sync::Arc};
2 2
3use anyhow::{bail, Context, Result}; 3use anyhow::Result;
4use console::Style; 4use fresh::fresh_login_or_signup;
5use dialoguer::theme::{ColorfulTheme, Theme}; 5use nostr::PublicKey;
6use nostr::{ 6use nostr_sdk::{NostrSigner, Timestamp, ToBech32};
7 nips::{nip05, nip46::NostrConnectURI},
8 PublicKey,
9};
10use nostr_connect::client::NostrConnect;
11use nostr_sdk::{
12 Alphabet, FromBech32, JsonUtil, Keys, Kind, NostrSigner, SingleLetterTag, Timestamp, ToBech32,
13 Url,
14};
15use qrcode::QrCode;
16use tokio::sync::{oneshot, Mutex};
17 7
18#[cfg(not(test))] 8#[cfg(not(test))]
19use crate::client::Client; 9use crate::client::Client;
20#[cfg(test)] 10#[cfg(test)]
21use crate::client::MockConnect; 11use crate::client::MockConnect;
22use crate::{ 12use crate::git::{Repo, RepoActions};
23 cli_interactor::{
24 Interactor, InteractorPrompt, Printer, PromptConfirmParms, PromptInputParms,
25 PromptPasswordParms,
26 },
27 client::{fetch_public_key, get_event_from_global_cache, Connect},
28 git::{Repo, RepoActions},
29};
30 13
14pub mod existing;
31mod key_encryption; 15mod key_encryption;
32use key_encryption::{decrypt_key, encrypt_key}; 16use existing::load_existing_login;
33mod user; 17pub mod user;
34use user::{UserMetadata, UserRef, UserRelayRef, UserRelays}; 18use user::UserRef;
35 19pub mod fresh;
36/// handles the encrpytion and storage of key material 20
37#[allow(clippy::too_many_arguments)] 21pub async fn login_or_signup(
38pub async fn launch( 22 git_repo: &Option<&Repo>,
39 git_repo: &Repo, 23 signer_info: &Option<SignerInfo>,
40 bunker_uri: &Option<String>,
41 bunker_app_key: &Option<String>,
42 nsec: &Option<String>,
43 password: &Option<String>, 24 password: &Option<String>,
44 #[cfg(test)] client: Option<&MockConnect>, 25 #[cfg(test)] client: Option<&MockConnect>,
45 #[cfg(not(test))] client: Option<&Client>, 26 #[cfg(not(test))] client: Option<&Client>,
46 change_user: bool, 27) -> Result<(Arc<dyn NostrSigner>, UserRef, SignerInfoSource)> {
47 silent: bool, 28 let res =
48) -> Result<(Arc<dyn NostrSigner>, UserRef)> { 29 load_existing_login(git_repo, signer_info, password, &None, client, false, true).await;
49 if let Ok((signer, public_key)) = match get_signer_without_prompts( 30 if res.is_ok() {
50 git_repo, 31 res
51 bunker_uri,
52 bunker_app_key,
53 nsec,
54 password,
55 change_user,
56 )
57 .await
58 {
59 Ok((signer, public_key)) => Ok((signer, public_key)),
60 Err(error) => {
61 if error
62 .to_string()
63 .eq("git config item nostr.nsec is an ncryptsec")
64 {
65 eprintln!(
66 "login as {}",
67 if let Ok(public_key) = PublicKey::from_bech32(
68 get_config_item(git_repo, "nostr.npub").unwrap_or("unknown".to_string()),
69 ) {
70 if let Ok(user_ref) =
71 get_user_details(&public_key, client, git_repo.get_path()?, silent)
72 .await
73 {
74 user_ref.metadata.name
75 } else {
76 "unknown ncryptsec".to_string()
77 }
78 } else {
79 "unknown ncryptsec".to_string()
80 }
81 );
82 loop {
83 // prompt for password
84 let password = Interactor::default()
85 .password(PromptPasswordParms::default().with_prompt("password"))
86 .context("failed to get password input from interactor.password")?;
87 if let Ok(keys) = get_keys_with_password(git_repo, &password) {
88 break Ok((Arc::new(keys) as Arc<dyn NostrSigner>, None));
89 }
90 eprintln!("incorrect password");
91 }
92 } else {
93 if nsec.is_some() {
94 bail!(error);
95 }
96 Err(error)
97 }
98 }
99 } {
100 let user_ref = get_user_details(
101 // Note: if rust-nostr NostrConnect::new() were updated to accept user public key as
102 // requested then the added complexity added in this commit can be undone
103 &(if let Some(public_key) = public_key {
104 public_key
105 } else {
106 signer
107 .get_public_key()
108 .await
109 .context("cannot get public key from signer")?
110 }),
111 client,
112 git_repo.get_path()?,
113 silent,
114 )
115 .await?;
116
117 if !silent {
118 print_logged_in_as(&user_ref, client.is_none())?;
119 }
120 Ok((signer, user_ref))
121 } else { 32 } else {
122 fresh_login(git_repo, client, change_user).await 33 fresh_login_or_signup(git_repo, client, false).await
123 } 34 }
124} 35}
125 36
126fn print_logged_in_as(user_ref: &UserRef, offline_mode: bool) -> Result<()> { 37#[derive(Clone)]
38pub enum SignerInfo {
39 Nsec {
40 nsec: String,
41 password: Option<String>,
42 npub: Option<String>,
43 },
44 Bunker {
45 bunker_uri: String,
46 bunker_app_key: String,
47 npub: Option<String>,
48 },
49}
50
51#[derive(PartialEq, Clone)]
52pub enum SignerInfoSource {
53 GitLocal,
54 GitGlobal,
55 CommandLineArguments,
56}
57
58fn print_logged_in_as(
59 user_ref: &UserRef,
60 offline_mode: bool,
61 source: &SignerInfoSource,
62) -> Result<()> {
127 if !offline_mode && user_ref.metadata.created_at.eq(&Timestamp::from(0)) { 63 if !offline_mode && user_ref.metadata.created_at.eq(&Timestamp::from(0)) {
128 eprintln!("cannot find profile..."); 64 eprintln!("cannot find profile...");
129 } else if !offline_mode && user_ref.metadata.name.eq(&user_ref.public_key.to_bech32()?) { 65 } else if !offline_mode && user_ref.metadata.name.eq(&user_ref.public_key.to_bech32()?) {
@@ -133,703 +69,21 @@ fn print_logged_in_as(user_ref: &UserRef, offline_mode: bool) -> Result<()> {
133 "cannot find your relay list. consider using another nostr client to create one to enhance your nostr experience." 69 "cannot find your relay list. consider using another nostr client to create one to enhance your nostr experience."
134 ); 70 );
135 } 71 }
136 eprintln!("logged in as {}", user_ref.metadata.name); 72 eprintln!(
137 Ok(()) 73 "logged in as {}{}",
138} 74 user_ref.metadata.name,
139 75 match source {
140async fn get_signer_without_prompts( 76 SignerInfoSource::CommandLineArguments => " via cli arguments",
141 git_repo: &Repo, 77 SignerInfoSource::GitLocal => " just to local repository",
142 bunker_uri: &Option<String>, 78 SignerInfoSource::GitGlobal => "",
143 bunker_app_key: &Option<String>,
144 nsec: &Option<String>,
145 password: &Option<String>,
146 save_local: bool,
147) -> Result<(Arc<dyn NostrSigner>, Option<PublicKey>)> {
148 if let Some(nsec) = nsec {
149 Ok((
150 Arc::new(get_keys_from_nsec(git_repo, nsec, password, save_local)?),
151 None,
152 ))
153 } else if let Some(password) = password {
154 Ok((Arc::new(get_keys_with_password(git_repo, password)?), None))
155 } else if let Some(bunker_uri) = bunker_uri {
156 if let Some(bunker_app_key) = bunker_app_key {
157 let signer = get_nip46_signer_from_uri_and_key(bunker_uri, bunker_app_key)
158 .await
159 .context("failed to connect with remote signer")?;
160 if save_local {
161 save_to_git_config(
162 git_repo,
163 &signer.get_public_key().await?.to_bech32()?,
164 &None,
165 &Some((bunker_uri.to_string(),bunker_app_key.to_string())),
166 false,
167 )
168 .context("failed to save bunker details local git config nostr.bunker-uri and nostr.bunker-app-key")?;
169 }
170 Ok((signer, None))
171 } else {
172 bail!(
173 "bunker-app-key parameter must be provided alongside bunker-uri. if unknown, login interactively."
174 )
175 }
176 } else if !save_local {
177 get_signer_with_git_config_nsec_or_bunker_without_prompts(git_repo).await
178 } else {
179 bail!("user wants prompts to specify new keys")
180 }
181}
182
183fn get_keys_from_nsec(
184 git_repo: &Repo,
185 nsec: &String,
186 password: &Option<String>,
187 save_local: bool,
188) -> Result<nostr::Keys> {
189 #[allow(unused_assignments)]
190 let mut s = String::new();
191 let keys = if nsec.contains("ncryptsec") {
192 s = nsec.to_string();
193 decrypt_key(
194 nsec,
195 password
196 .clone()
197 .context("password must be supplied when using ncryptsec as nsec parameter")?
198 .as_str(),
199 )
200 .context("failed to decrypt key with provided password")
201 .context("failed to decrypt ncryptsec supplied as nsec with password")?
202 } else {
203 s = nsec.to_string();
204 nostr::Keys::from_str(nsec).context("invalid nsec parameter")?
205 };
206 if save_local {
207 if let Some(password) = password {
208 s = encrypt_key(&keys, password)?;
209 }
210 save_to_git_config(
211 git_repo,
212 &keys.public_key().to_bech32()?,
213 &Some(s),
214 &None,
215 false,
216 )
217 .context("failed to save encrypted nsec in local git config nostr.nsec")?;
218 }
219 Ok(keys)
220}
221
222fn save_to_git_config(
223 git_repo: &Repo,
224 npub: &str,
225 nsec: &Option<String>,
226 bunker: &Option<(String, String)>,
227 global: bool,
228) -> Result<()> {
229 if let Err(error) = silently_save_to_git_config(git_repo, npub, nsec, bunker, global) {
230 eprintln!(
231 "failed to save login details to {} git config",
232 if global { "global" } else { "local" }
233 );
234 if let Some(nsec) = nsec {
235 if nsec.contains("ncryptsec") {
236 eprintln!("manually set git config nostr.nsec to: {nsec}");
237 } else {
238 eprintln!("manually set git config nostr.nsec");
239 }
240 }
241 if let Some(bunker) = bunker {
242 eprintln!("manually set git config as follows:");
243 eprintln!("nostr.bunker-uri: {}", bunker.0);
244 eprintln!("nostr.bunker-app-key: {}", bunker.1);
245 }
246 Err(error)
247 } else {
248 eprintln!(
249 "saved login details to {} git config",
250 if global { "global" } else { "local" }
251 );
252 Ok(())
253 }
254}
255fn silently_save_to_git_config(
256 git_repo: &Repo,
257 npub: &str,
258 nsec: &Option<String>,
259 bunker: &Option<(String, String)>,
260 global: bool,
261) -> Result<()> {
262 // must do this first otherwise it might remove the global items just added
263 if global {
264 git_repo.remove_git_config_item("nostr.npub", false)?;
265 git_repo.remove_git_config_item("nostr.nsec", false)?;
266 git_repo.remove_git_config_item("nostr.bunker-uri", false)?;
267 git_repo.remove_git_config_item("nostr.bunker-app-key", false)?;
268 }
269 if let Some(bunker) = bunker {
270 git_repo.remove_git_config_item("nostr.nsec", global)?;
271 git_repo.save_git_config_item("nostr.bunker-uri", &bunker.0, global)?;
272 git_repo.save_git_config_item("nostr.bunker-app-key", &bunker.1, global)?;
273 }
274 if let Some(nsec) = nsec {
275 git_repo.save_git_config_item("nostr.nsec", nsec, global)?;
276 git_repo.remove_git_config_item("nostr.bunker-uri", global)?;
277 git_repo.remove_git_config_item("nostr.bunker-app-key", global)?;
278 }
279 git_repo.save_git_config_item("nostr.npub", npub, global)
280}
281
282fn get_keys_with_password(git_repo: &Repo, password: &str) -> Result<nostr::Keys> {
283 decrypt_key(
284 &git_repo
285 .get_git_config_item("nostr.nsec", None)
286 .context("failed get git config")?
287 .context("git config item nostr.nsec doesn't exist so cannot decrypt it")?,
288 password,
289 )
290 .context("failed to decrypt stored nsec key with provided password")
291}
292
293async fn get_nip46_signer_from_uri_and_key(
294 uri: &str,
295 app_key: &str,
296) -> Result<Arc<dyn NostrSigner>> {
297 let term = console::Term::stderr();
298 term.write_line("connecting to remote signer...")?;
299 let uri = NostrConnectURI::parse(uri)?;
300 let signer = Arc::new(NostrConnect::new(
301 uri,
302 nostr::Keys::from_str(app_key).context("invalid app key")?,
303 Duration::from_secs(10 * 60),
304 None,
305 )?);
306 term.clear_last_lines(1)?;
307 Ok(signer)
308}
309
310async fn get_signer_with_git_config_nsec_or_bunker_without_prompts(
311 git_repo: &Repo,
312) -> Result<(Arc<dyn NostrSigner>, Option<PublicKey>)> {
313 if let Ok(local_nsec) = &git_repo
314 .get_git_config_item("nostr.nsec", Some(false))
315 .context("failed get local git config")?
316 .context("git local config item nostr.nsec doesn't exist")
317 {
318 if local_nsec.contains("ncryptsec") {
319 bail!("git global config item nostr.nsec is an ncryptsec")
320 }
321 Ok((
322 Arc::new(nostr::Keys::from_str(local_nsec).context("invalid nsec parameter")?),
323 None,
324 ))
325 } else if let Ok((uri, app_key, npub)) =
326 get_git_config_bunker_uri_and_app_key(git_repo, Some(false))
327 {
328 Ok((
329 get_nip46_signer_from_uri_and_key(&uri, &app_key).await?,
330 if let Ok(pubic_key) = PublicKey::parse(npub) {
331 Some(pubic_key)
332 } else {
333 None
334 },
335 ))
336 } else if let Ok(global_nsec) = &git_repo
337 .get_git_config_item("nostr.nsec", Some(true))
338 .context("failed get global git config")?
339 .context("git global config item nostr.nsec doesn't exist")
340 {
341 if global_nsec.contains("ncryptsec") {
342 bail!("git global config item nostr.nsec is an ncryptsec")
343 }
344 Ok((
345 Arc::new(nostr::Keys::from_str(global_nsec).context("invalid nsec parameter")?),
346 None,
347 ))
348 } else if let Ok((uri, app_key, npub)) =
349 get_git_config_bunker_uri_and_app_key(git_repo, Some(true))
350 {
351 Ok((
352 get_nip46_signer_from_uri_and_key(&uri, &app_key).await?,
353 if let Ok(pubic_key) = PublicKey::parse(npub) {
354 Some(pubic_key)
355 } else {
356 None
357 },
358 ))
359 } else {
360 bail!("cannot get nsec or bunker from git config")
361 }
362}
363
364fn get_git_config_bunker_uri_and_app_key(
365 git_repo: &Repo,
366 global: Option<bool>,
367) -> Result<(String, String, String)> {
368 Ok((
369 git_repo
370 .get_git_config_item("nostr.bunker-uri", global)
371 .context("failed get local git config")?
372 .context("git local config item nostr.bunker-uri doesn't exist")?
373 .to_string(),
374 git_repo
375 .get_git_config_item("nostr.bunker-app-key", global)
376 .context("failed get local git config")?
377 .context("git local config item nostr.bunker-app-key doesn't exist")?
378 .to_string(),
379 git_repo
380 .get_git_config_item("nostr.npub", global)
381 .context("failed get local git config")?
382 .context("git local config item nostr.npub doesn't exist")?
383 .to_string(),
384 ))
385}
386
387async fn fresh_login(
388 git_repo: &Repo,
389 #[cfg(test)] client: Option<&MockConnect>,
390 #[cfg(not(test))] client: Option<&Client>,
391 always_save: bool,
392) -> Result<(Arc<dyn NostrSigner>, UserRef)> {
393 let app_key = Keys::generate();
394 let app_key_secret = app_key.secret_key().to_secret_hex();
395 let relays = if let Some(client) = client {
396 client
397 .get_fallback_signer_relays()
398 .iter()
399 .flat_map(|s| Url::parse(s))
400 .collect::<Vec<Url>>()
401 } else {
402 vec![]
403 };
404 let offline = client.is_none();
405 let nostr_connect_url = NostrConnectURI::client(app_key.public_key(), relays.clone(), "ngit");
406 let qr = generate_qr(&nostr_connect_url.to_string())?;
407
408 let printer = Arc::new(Mutex::new(Printer::default()));
409 if !offline {
410 let printer_clone = Arc::clone(&printer);
411 let mut printer_locked = printer_clone.lock().await;
412 printer_locked.printlns(qr);
413 printer_locked.println(format!(
414 "scan QR or paste into remote signer: {nostr_connect_url}"
415 ));
416 printer_locked.println_with_custom_formatting(
417 {
418 let mut s = String::new();
419 let _ = ColorfulTheme::default().format_confirm_prompt(
420 &mut s,
421 "login with nsec / bunker url / nostr address instead",
422 Some(true),
423 );
424 s
425 },
426 "? login with nsec / bunker url / nostr address instead? (y/n) › yes".to_string(),
427 );
428 }
429
430 let (tx, rx) = oneshot::channel();
431 let printer_clone = Arc::clone(&printer);
432
433 let qr_listener = tokio::spawn(async move {
434 if offline {
435 return;
436 }
437 if let Ok(nostr_connect) = NostrConnect::new(
438 nostr_connect_url.clone(),
439 app_key.clone(),
440 Duration::from_secs(10 * 60),
441 None,
442 ) {
443 let signer: Arc<dyn NostrSigner> = Arc::new(nostr_connect);
444 if let Ok(pub_key) = fetch_public_key(&signer).await {
445 let mut printer_locked = printer_clone.lock().await;
446 printer_locked.clear_all();
447
448 printer_locked.println_with_custom_formatting(
449 format!(
450 "{}",
451 Style::new().bold().apply_to("connected to remote signer"),
452 ),
453 "connected to remote signer".to_string(),
454 );
455 printer_locked.println("press any key to continue...".to_string());
456 let _ = tx.send(Some((signer, pub_key)));
457 }
458 }
459 });
460 if !offline {
461 let _ = console::Term::stderr().read_char();
462 }
463 qr_listener.abort();
464 let printer_clone = Arc::clone(&printer);
465 let mut printer = printer_clone.lock().await;
466 printer.clear_all();
467
468 let (signer, public_key) = {
469 if let Ok(Some((signer, public_key))) = rx.await {
470 let bunker_url = NostrConnectURI::Bunker {
471 remote_signer_public_key: public_key,
472 relays: relays.clone(),
473 secret: None,
474 };
475 if let Err(error) = save_bunker(
476 git_repo,
477 &public_key,
478 &bunker_url.to_string(),
479 &app_key_secret,
480 always_save,
481 ) {
482 eprintln!("{error}");
483 }
484 (signer, public_key)
485 } else {
486 let mut public_key: Option<PublicKey> = None;
487 // prompt for nsec
488 let mut prompt = "login with nsec / bunker url / nostr address";
489 let signer: Arc<dyn NostrSigner> = loop {
490 let input = Interactor::default()
491 .input(PromptInputParms::default().with_prompt(prompt))
492 .context("failed to get nsec input from interactor")?;
493 if let Ok(keys) = nostr::Keys::from_str(&input) {
494 if let Err(error) = save_keys(git_repo, &keys, always_save) {
495 eprintln!("{error}");
496 }
497 break Arc::new(keys);
498 }
499 let uri = if let Ok(uri) = NostrConnectURI::parse(&input) {
500 uri
501 } else if input.contains('@') {
502 if let Ok(uri) = fetch_nip46_uri_from_nip05(&input).await {
503 uri
504 } else {
505 prompt = "failed. try again with nostr address / bunker uri / nsec";
506 continue;
507 }
508 } else {
509 prompt = "invalid. try again with nostr address / bunker uri / nsec";
510 continue;
511 };
512 match get_nip46_signer_from_uri_and_key(&uri.to_string(), &app_key_secret).await {
513 Ok(signer) => {
514 let pub_key = fetch_public_key(&signer).await?;
515 if let Err(error) = save_bunker(
516 git_repo,
517 &pub_key,
518 &uri.to_string(),
519 &app_key_secret,
520 always_save,
521 ) {
522 eprintln!("{error}");
523 }
524 public_key = Some(pub_key);
525 break signer;
526 }
527 Err(_) => {
528 prompt = "failed. try again with nostr address / bunker uri / nsec";
529 }
530 }
531 };
532 let public_key = if let Some(public_key) = public_key {
533 public_key
534 } else {
535 signer.get_public_key().await?
536 };
537 (signer, public_key)
538 }
539 };
540 // lookup profile
541 let user_ref = get_user_details(&public_key, client, git_repo.get_path()?, false).await?;
542 print_logged_in_as(&user_ref, client.is_none())?;
543 Ok((signer, user_ref))
544}
545
546fn generate_qr(data: &str) -> Result<Vec<String>> {
547 let mut lines = vec![];
548 let qr =
549 QrCode::new(data.as_bytes()).context("failed to create QR of nostrconnect login url")?;
550 let colors = qr.to_colors();
551 let rows: Vec<&[qrcode::Color]> = colors.chunks(qr.width()).collect();
552 for (row, data) in rows.iter().enumerate() {
553 let odd = row % 2 != 0;
554 if odd {
555 continue;
556 }
557 let mut line = String::new();
558 for (col, color) in data.iter().enumerate() {
559 let top = color;
560 let mut bottom = qrcode::Color::Light;
561 if let Some(next_row_data) = rows.get(row + 1) {
562 if let Some(color) = next_row_data.get(col) {
563 bottom = *color;
564 }
565 }
566 line.push(if *top == qrcode::Color::Dark {
567 if bottom == qrcode::Color::Dark {
568 '█'
569 } else {
570 '▀'
571 }
572 } else if bottom == qrcode::Color::Dark {
573 '▄'
574 } else {
575 ' '
576 });
577 }
578 lines.push(line);
579 }
580 Ok(lines)
581}
582
583pub async fn fetch_nip46_uri_from_nip05(nip05: &str) -> Result<NostrConnectURI> {
584 let term = console::Term::stderr();
585 term.write_line("contacting login service provider...")?;
586 let res = nip05::profile(&nip05, None).await;
587 term.clear_last_lines(1)?;
588 match res {
589 Ok(profile) => {
590 if profile.nip46.is_empty() {
591 eprintln!("nip05 provider isn't configured for remote login");
592 bail!("nip05 provider isn't configured for remote login")
593 }
594 Ok(NostrConnectURI::Bunker {
595 remote_signer_public_key: profile.public_key,
596 relays: profile.nip46,
597 secret: None,
598 })
599 } 79 }
600 Err(error) => { 80 );
601 eprintln!("error contacting login service provider: {error}");
602 Err(error).context("error contacting login service provider")
603 }
604 }
605}
606
607fn save_bunker(
608 git_repo: &Repo,
609 public_key: &PublicKey,
610 uri: &str,
611 app_key: &str,
612 always_save: bool,
613) -> Result<()> {
614 if always_save
615 || Interactor::default()
616 .confirm(PromptConfirmParms::default().with_prompt("save login details?"))?
617 {
618 let global = !Interactor::default().confirm(
619 PromptConfirmParms::default()
620 .with_prompt("save login just for this repository?")
621 .with_default(false),
622 )?;
623 let npub = public_key.to_bech32()?;
624 if let Err(error) = save_to_git_config(
625 git_repo,
626 &npub,
627 &None,
628 &Some((uri.to_string(), app_key.to_string())),
629 global,
630 ) {
631 if global {
632 if Interactor::default().confirm(
633 PromptConfirmParms::default()
634 .with_prompt("save in repository git config?")
635 .with_default(true),
636 )? {
637 save_to_git_config(
638 git_repo,
639 &npub,
640 &None,
641 &Some((uri.to_string(), app_key.to_string())),
642 false,
643 )?;
644 }
645 } else {
646 Err(error)?;
647 }
648 };
649 }
650 Ok(())
651}
652
653fn save_keys(git_repo: &Repo, keys: &nostr::Keys, always_save: bool) -> Result<()> {
654 if always_save
655 || Interactor::default()
656 .confirm(PromptConfirmParms::default().with_prompt("save login details?"))?
657 {
658 let global = !Interactor::default().confirm(
659 PromptConfirmParms::default()
660 .with_prompt("just for this repository?")
661 .with_default(false),
662 )?;
663
664 let encrypt = Interactor::default().confirm(
665 PromptConfirmParms::default()
666 .with_prompt("require password?")
667 .with_default(false),
668 )?;
669
670 let npub = keys.public_key().to_bech32()?;
671 let nsec_string = if encrypt {
672 let password = Interactor::default()
673 .password(
674 PromptPasswordParms::default()
675 .with_prompt("encrypt with password")
676 .with_confirm(),
677 )
678 .context("failed to get password input from interactor.password")?;
679 encrypt_key(keys, &password)?
680 } else {
681 keys.secret_key().to_bech32()?
682 };
683
684 if let Err(error) =
685 save_to_git_config(git_repo, &npub, &Some(nsec_string.clone()), &None, global)
686 {
687 if global {
688 if Interactor::default().confirm(
689 PromptConfirmParms::default()
690 .with_prompt("save in repository git config?")
691 .with_default(true),
692 )? {
693 save_to_git_config(git_repo, &npub, &Some(nsec_string.clone()), &None, false)?;
694 }
695 } else {
696 eprintln!("{error}");
697 Err(error)?;
698 }
699 };
700 };
701 Ok(()) 81 Ok(())
702} 82}
703 83
704fn get_config_item(git_repo: &Repo, name: &str) -> Result<String> {
705 git_repo
706 .get_git_config_item(name, None)
707 .context("failed get git config")?
708 .context(format!("git config item {name} doesn't exist"))
709}
710
711fn extract_user_metadata(
712 public_key: &nostr::PublicKey,
713 events: &[nostr::Event],
714) -> Result<UserMetadata> {
715 let event = events
716 .iter()
717 .filter(|e| e.kind.eq(&nostr::Kind::Metadata) && e.pubkey.eq(public_key))
718 .max_by_key(|e| e.created_at);
719
720 let metadata: Option<nostr::Metadata> = if let Some(event) = event {
721 Some(
722 nostr::Metadata::from_json(event.content.clone())
723 .context("metadata cannot be found in kind 0 event content")?,
724 )
725 } else {
726 None
727 };
728
729 Ok(UserMetadata {
730 name: if let Some(metadata) = metadata {
731 if let Some(n) = metadata.name {
732 n
733 } else if let Some(n) = metadata.custom.get("displayName") {
734 // strip quote marks that custom.get() adds
735 let binding = n.to_string();
736 let mut chars = binding.chars();
737 chars.next();
738 chars.next_back();
739 chars.as_str().to_string()
740 } else if let Some(n) = metadata.display_name {
741 n
742 } else {
743 public_key.to_bech32()?
744 }
745 } else {
746 public_key.to_bech32()?
747 },
748 created_at: if let Some(event) = event {
749 event.created_at
750 } else {
751 Timestamp::from(0)
752 },
753 })
754}
755
756fn extract_user_relays(public_key: &nostr::PublicKey, events: &[nostr::Event]) -> UserRelays {
757 let event = events
758 .iter()
759 .filter(|e| e.kind.eq(&nostr::Kind::RelayList) && e.pubkey.eq(public_key))
760 .max_by_key(|e| e.created_at);
761
762 UserRelays {
763 relays: if let Some(event) = event {
764 event
765 .tags
766 .iter()
767 .filter(|t| {
768 t.kind()
769 .eq(&nostr::TagKind::SingleLetter(SingleLetterTag::lowercase(
770 Alphabet::R,
771 )))
772 })
773 .map(|t| UserRelayRef {
774 url: t.as_slice()[1].clone(),
775 read: t.as_slice().len() == 2 || t.as_slice()[2].eq("read"),
776 write: t.as_slice().len() == 2 || t.as_slice()[2].eq("write"),
777 })
778 .collect()
779 } else {
780 vec![]
781 },
782 created_at: if let Some(event) = event {
783 event.created_at
784 } else {
785 Timestamp::from(0)
786 },
787 }
788}
789
790async fn get_user_details(
791 public_key: &PublicKey,
792 #[cfg(test)] client: Option<&crate::client::MockConnect>,
793 #[cfg(not(test))] client: Option<&Client>,
794 git_repo_path: &Path,
795 cache_only: bool,
796) -> Result<UserRef> {
797 if let Ok(user_ref) = get_user_ref_from_cache(git_repo_path, public_key).await {
798 Ok(user_ref)
799 } else {
800 let empty = UserRef {
801 public_key: public_key.to_owned(),
802 metadata: extract_user_metadata(public_key, &[])?,
803 relays: extract_user_relays(public_key, &[]),
804 };
805 if cache_only {
806 Ok(empty)
807 } else if let Some(client) = client {
808 let term = console::Term::stderr();
809 term.write_line("searching for profile...")?;
810 let (_, progress_reporter) = client
811 .fetch_all(
812 git_repo_path,
813 &HashSet::new(),
814 &HashSet::from_iter(vec![*public_key]),
815 )
816 .await?;
817 if let Ok(user_ref) = get_user_ref_from_cache(git_repo_path, public_key).await {
818 progress_reporter.clear()?;
819 // if std::env::var("NGITTEST").is_err() {term.clear_last_lines(1)?;}
820 Ok(user_ref)
821 } else {
822 Ok(empty)
823 }
824 } else {
825 Ok(empty)
826 }
827 }
828}
829
830// None: in the edge case where the user is logged in via cli arguments rather 84// None: in the edge case where the user is logged in via cli arguments rather
831// than from git config this may be wrong. TODO: fix this 85// than from git config this may be wrong. TODO: fix this
832pub async fn get_logged_in_user(git_repo_path: &Path) -> Result<Option<PublicKey>> { 86pub async fn get_likely_logged_in_user(git_repo_path: &Path) -> Result<Option<PublicKey>> {
833 let git_repo = Repo::from_path(&git_repo_path.to_path_buf())?; 87 let git_repo = Repo::from_path(&git_repo_path.to_path_buf())?;
834 Ok( 88 Ok(
835 if let Some(npub) = git_repo.get_git_config_item("nostr.npub", None)? { 89 if let Some(npub) = git_repo.get_git_config_item("nostr.npub", None)? {
@@ -844,31 +98,6 @@ pub async fn get_logged_in_user(git_repo_path: &Path) -> Result<Option<PublicKey
844 ) 98 )
845} 99}
846 100
847pub async fn get_user_ref_from_cache(
848 git_repo_path: &Path,
849 public_key: &PublicKey,
850) -> Result<UserRef> {
851 let filters = vec![
852 nostr::Filter::default()
853 .author(*public_key)
854 .kind(Kind::Metadata),
855 nostr::Filter::default()
856 .author(*public_key)
857 .kind(Kind::RelayList),
858 ];
859
860 let events = get_event_from_global_cache(git_repo_path, filters.clone()).await?;
861
862 if events.is_empty() {
863 bail!("no metadata and profile list in cache for selected public key");
864 }
865 Ok(UserRef {
866 public_key: public_key.to_owned(),
867 metadata: extract_user_metadata(public_key, &events)?,
868 relays: extract_user_relays(public_key, &events),
869 })
870}
871
872pub fn get_curent_user(git_repo: &Repo) -> Result<Option<PublicKey>> { 101pub fn get_curent_user(git_repo: &Repo) -> Result<Option<PublicKey>> {
873 Ok( 102 Ok(
874 if let Some(npub) = git_repo.get_git_config_item("nostr.npub", None)? { 103 if let Some(npub) = git_repo.get_git_config_item("nostr.npub", None)? {