upleb.uk

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

summaryrefslogtreecommitdiff
path: root/src/lib
diff options
context:
space:
mode:
Diffstat (limited to 'src/lib')
-rw-r--r--src/lib/cli_interactor.rs48
-rw-r--r--src/lib/client.rs16
-rw-r--r--src/lib/login/mod.rs233
3 files changed, 253 insertions, 44 deletions
diff --git a/src/lib/cli_interactor.rs b/src/lib/cli_interactor.rs
index 4cf6357..dcaccf1 100644
--- a/src/lib/cli_interactor.rs
+++ b/src/lib/cli_interactor.rs
@@ -1,5 +1,6 @@
1use anyhow::{Context, Result}; 1use anyhow::{Context, Result};
2use dialoguer::{theme::ColorfulTheme, Confirm, Input, Password}; 2use dialoguer::{theme::ColorfulTheme, Confirm, Input, Password};
3use indicatif::TermLike;
3#[cfg(test)] 4#[cfg(test)]
4use mockall::*; 5use mockall::*;
5 6
@@ -184,3 +185,50 @@ impl PromptMultiChoiceParms {
184 self 185 self
185 } 186 }
186} 187}
188
189#[derive(Debug, Default)]
190pub struct Printer {
191 printed_lines: Vec<String>,
192}
193impl Printer {
194 pub fn println(&mut self, line: String) {
195 eprintln!("{line}");
196 self.printed_lines.push(line);
197 }
198 pub fn println_with_custom_formatting(
199 &mut self,
200 line: String,
201 line_without_formatting: String,
202 ) {
203 eprintln!("{line}");
204 self.printed_lines.push(line_without_formatting);
205 }
206 pub fn printlns(&mut self, lines: Vec<String>) {
207 for line in lines {
208 self.println(line);
209 }
210 }
211 pub fn clear_all(&mut self) {
212 let term = console::Term::stderr();
213 let _ = term.clear_last_lines(count_lines_per_msg_vec(
214 term.width(),
215 &self.printed_lines,
216 0,
217 ));
218 self.printed_lines.drain(..);
219 }
220}
221
222pub fn count_lines_per_msg(width: u16, msg: &str, prefix_len: usize) -> usize {
223 if width == 0 {
224 return 1;
225 }
226 // ((msg_len+prefix) / width).ceil() implemented using Integer Arithmetic
227 ((msg.chars().count() + prefix_len) + (width - 1) as usize) / width as usize
228}
229
230pub fn count_lines_per_msg_vec(width: u16, msgs: &[String], prefix_len: usize) -> usize {
231 msgs.iter()
232 .map(|msg| count_lines_per_msg(width, msg, prefix_len))
233 .sum()
234}
diff --git a/src/lib/client.rs b/src/lib/client.rs
index 455725c..59e17f2 100644
--- a/src/lib/client.rs
+++ b/src/lib/client.rs
@@ -53,6 +53,7 @@ pub struct Client {
53 fallback_relays: Vec<String>, 53 fallback_relays: Vec<String>,
54 more_fallback_relays: Vec<String>, 54 more_fallback_relays: Vec<String>,
55 blaster_relays: Vec<String>, 55 blaster_relays: Vec<String>,
56 fallback_signer_relays: Vec<String>,
56} 57}
57 58
58#[cfg_attr(test, automock)] 59#[cfg_attr(test, automock)]
@@ -66,6 +67,7 @@ pub trait Connect {
66 fn get_fallback_relays(&self) -> &Vec<String>; 67 fn get_fallback_relays(&self) -> &Vec<String>;
67 fn get_more_fallback_relays(&self) -> &Vec<String>; 68 fn get_more_fallback_relays(&self) -> &Vec<String>;
68 fn get_blaster_relays(&self) -> &Vec<String>; 69 fn get_blaster_relays(&self) -> &Vec<String>;
70 fn get_fallback_signer_relays(&self) -> &Vec<String>;
69 async fn send_event_to( 71 async fn send_event_to(
70 &self, 72 &self,
71 git_repo_path: &Path, 73 git_repo_path: &Path,
@@ -132,6 +134,13 @@ impl Connect for Client {
132 } else { 134 } else {
133 vec!["wss://nostr.mutinywallet.com".to_string()] 135 vec!["wss://nostr.mutinywallet.com".to_string()]
134 }; 136 };
137
138 let fallback_signer_relays: Vec<String> = if std::env::var("NGITTEST").is_ok() {
139 vec!["ws://localhost:8051".to_string()]
140 } else {
141 vec!["wss://relay.nsec.app".to_string()]
142 };
143
135 Client { 144 Client {
136 client: nostr_sdk::ClientBuilder::new() 145 client: nostr_sdk::ClientBuilder::new()
137 .opts(Options::new().relay_limits(RelayLimits::disable())) 146 .opts(Options::new().relay_limits(RelayLimits::disable()))
@@ -139,6 +148,7 @@ impl Connect for Client {
139 fallback_relays, 148 fallback_relays,
140 more_fallback_relays, 149 more_fallback_relays,
141 blaster_relays, 150 blaster_relays,
151 fallback_signer_relays,
142 } 152 }
143 } 153 }
144 fn new(opts: Params) -> Self { 154 fn new(opts: Params) -> Self {
@@ -153,6 +163,7 @@ impl Connect for Client {
153 fallback_relays: opts.fallback_relays, 163 fallback_relays: opts.fallback_relays,
154 more_fallback_relays: opts.more_fallback_relays, 164 more_fallback_relays: opts.more_fallback_relays,
155 blaster_relays: opts.blaster_relays, 165 blaster_relays: opts.blaster_relays,
166 fallback_signer_relays: opts.fallback_signer_relays,
156 } 167 }
157 } 168 }
158 169
@@ -198,6 +209,10 @@ impl Connect for Client {
198 &self.blaster_relays 209 &self.blaster_relays
199 } 210 }
200 211
212 fn get_fallback_signer_relays(&self) -> &Vec<String> {
213 &self.fallback_signer_relays
214 }
215
201 async fn send_event_to( 216 async fn send_event_to(
202 &self, 217 &self,
203 git_repo_path: &Path, 218 git_repo_path: &Path,
@@ -629,6 +644,7 @@ pub struct Params {
629 pub fallback_relays: Vec<String>, 644 pub fallback_relays: Vec<String>,
630 pub more_fallback_relays: Vec<String>, 645 pub more_fallback_relays: Vec<String>,
631 pub blaster_relays: Vec<String>, 646 pub blaster_relays: Vec<String>,
647 pub fallback_signer_relays: Vec<String>,
632} 648}
633 649
634fn get_dedup_events(relay_results: Vec<Result<Vec<nostr::Event>>>) -> Vec<Event> { 650fn get_dedup_events(relay_results: Vec<Result<Vec<nostr::Event>>>) -> Vec<Event> {
diff --git a/src/lib/login/mod.rs b/src/lib/login/mod.rs
index b6e7623..825ec30 100644
--- a/src/lib/login/mod.rs
+++ b/src/lib/login/mod.rs
@@ -1,14 +1,19 @@
1use std::{collections::HashSet, path::Path, str::FromStr, time::Duration}; 1use std::{collections::HashSet, path::Path, str::FromStr, sync::Arc, time::Duration};
2 2
3use anyhow::{bail, Context, Result}; 3use anyhow::{bail, Context, Result};
4use console::Style;
5use dialoguer::theme::{ColorfulTheme, Theme};
4use nostr::{ 6use nostr::{
5 nips::{nip05, nip46::NostrConnectURI}, 7 nips::{nip05, nip46::NostrConnectURI},
6 PublicKey, 8 PublicKey,
7}; 9};
8use nostr_sdk::{ 10use nostr_sdk::{
9 Alphabet, FromBech32, JsonUtil, Keys, Kind, NostrSigner, SingleLetterTag, Timestamp, ToBech32, 11 Alphabet, FromBech32, JsonUtil, Keys, Kind, NostrSigner, SingleLetterTag, Timestamp, ToBech32,
12 Url,
10}; 13};
11use nostr_signer::Nip46Signer; 14use nostr_signer::Nip46Signer;
15use qrcode::QrCode;
16use tokio::sync::{oneshot, Mutex};
12 17
13#[cfg(not(test))] 18#[cfg(not(test))]
14use crate::client::Client; 19use crate::client::Client;
@@ -16,7 +21,8 @@ use crate::client::Client;
16use crate::client::MockConnect; 21use crate::client::MockConnect;
17use crate::{ 22use crate::{
18 cli_interactor::{ 23 cli_interactor::{
19 Interactor, InteractorPrompt, PromptConfirmParms, PromptInputParms, PromptPasswordParms, 24 Interactor, InteractorPrompt, Printer, PromptConfirmParms, PromptInputParms,
25 PromptPasswordParms,
20 }, 26 },
21 client::{fetch_public_key, get_event_from_global_cache, Connect}, 27 client::{fetch_public_key, get_event_from_global_cache, Connect},
22 git::{Repo, RepoActions}, 28 git::{Repo, RepoActions},
@@ -356,60 +362,198 @@ async fn fresh_login(
356 #[cfg(not(test))] client: Option<&Client>, 362 #[cfg(not(test))] client: Option<&Client>,
357 always_save: bool, 363 always_save: bool,
358) -> Result<(NostrSigner, UserRef)> { 364) -> Result<(NostrSigner, UserRef)> {
359 let mut public_key: Option<PublicKey> = None; 365 let app_key = Keys::generate();
360 // prompt for nsec 366 let app_key_secret = app_key.secret_key()?.to_secret_hex();
361 let mut prompt = "login with nostr address / nsec"; 367 let relays = if let Some(client) = client {
362 let signer = loop { 368 client
363 let input = Interactor::default() 369 .get_fallback_signer_relays()
364 .input(PromptInputParms::default().with_prompt(prompt)) 370 .iter()
365 .context("failed to get nsec input from interactor")?; 371 .flat_map(|s| Url::parse(s))
366 if let Ok(keys) = nostr::Keys::from_str(&input) { 372 .collect::<Vec<Url>>()
367 if let Err(error) = save_keys(git_repo, &keys, always_save) { 373 } else {
368 eprintln!("{error}"); 374 vec![]
375 };
376 let offline = client.is_none();
377 let nostr_connect_url = NostrConnectURI::client(app_key.public_key(), relays.clone(), "ngit");
378 let qr = generate_qr(&nostr_connect_url.to_string())?;
379
380 let printer = Arc::new(Mutex::new(Printer::default()));
381 if !offline {
382 let printer_clone = Arc::clone(&printer);
383 let mut printer_locked = printer_clone.lock().await;
384 printer_locked.printlns(qr);
385 printer_locked.println(format!(
386 "scan QR or paste into remote signer: {nostr_connect_url}"
387 ));
388 printer_locked.println_with_custom_formatting(
389 {
390 let mut s = String::new();
391 let _ = ColorfulTheme::default().format_confirm_prompt(
392 &mut s,
393 "login with nsec / bunker url / nostr address instead",
394 Some(true),
395 );
396 s
397 },
398 "? login with nsec / bunker url / nostr address instead? (y/n) › yes".to_string(),
399 );
400 }
401
402 let (tx, rx) = oneshot::channel();
403 let printer_clone = Arc::clone(&printer);
404
405 let qr_listener = tokio::spawn(async move {
406 if offline {
407 return;
408 }
409 if let Ok(nip46_signer) = Nip46Signer::new(
410 nostr_connect_url.clone(),
411 app_key.clone(),
412 Duration::from_secs(10 * 60),
413 None,
414 )
415 .await
416 {
417 let signer = NostrSigner::nip46(nip46_signer);
418 if let Ok(pub_key) = fetch_public_key(&signer).await {
419 let mut printer_locked = printer_clone.lock().await;
420 printer_locked.clear_all();
421
422 printer_locked.println_with_custom_formatting(
423 format!(
424 "{}",
425 Style::new().bold().apply_to("connected to remote signer"),
426 ),
427 "connected to remote signer".to_string(),
428 );
429 printer_locked.println("press any key to continue...".to_string());
430 let _ = tx.send(Some((signer, pub_key)));
369 } 431 }
370 break NostrSigner::Keys(keys);
371 } 432 }
372 let uri = if let Ok(uri) = NostrConnectURI::parse(&input) { 433 });
373 uri 434 if !offline {
374 } else if input.contains('@') { 435 let _ = console::Term::stderr().read_char();
375 if let Ok(uri) = fetch_nip46_uri_from_nip05(&input).await { 436 }
376 uri 437 qr_listener.abort();
377 } else { 438 let printer_clone = Arc::clone(&printer);
378 prompt = "failed. try again with nostr address / bunker uri / nsec"; 439 let mut printer = printer_clone.lock().await;
379 continue; 440 printer.clear_all();
441
442 let (signer, public_key) = {
443 if let Ok(Some((signer, public_key))) = rx.await {
444 let bunker_url = NostrConnectURI::Bunker {
445 signer_public_key: public_key,
446 relays: relays.clone(),
447 secret: None,
448 };
449 if let Err(error) = save_bunker(
450 git_repo,
451 &public_key,
452 &bunker_url.to_string(),
453 &app_key_secret,
454 always_save,
455 ) {
456 eprintln!("{error}");
380 } 457 }
458 (signer, public_key)
381 } else { 459 } else {
382 prompt = "invalid. try again with nostr address / bunker uri / nsec"; 460 let mut public_key: Option<PublicKey> = None;
383 continue; 461 // prompt for nsec
384 }; 462 let mut prompt = "login with nsec / bunker url / nostr address";
385 let app_key = Keys::generate().secret_key()?.to_secret_hex(); 463 let signer = loop {
386 match get_nip46_signer_from_uri_and_key(&uri.to_string(), &app_key).await { 464 let input = Interactor::default()
387 Ok(signer) => { 465 .input(PromptInputParms::default().with_prompt(prompt))
388 let pub_key = fetch_public_key(&signer).await?; 466 .context("failed to get nsec input from interactor")?;
389 if let Err(error) = 467 if let Ok(keys) = nostr::Keys::from_str(&input) {
390 save_bunker(git_repo, &pub_key, &uri.to_string(), &app_key, always_save) 468 if let Err(error) = save_keys(git_repo, &keys, always_save) {
391 { 469 eprintln!("{error}");
392 eprintln!("{error}"); 470 }
471 break NostrSigner::Keys(keys);
393 } 472 }
394 public_key = Some(pub_key); 473 let uri = if let Ok(uri) = NostrConnectURI::parse(&input) {
395 break signer; 474 uri
396 } 475 } else if input.contains('@') {
397 Err(_) => { 476 if let Ok(uri) = fetch_nip46_uri_from_nip05(&input).await {
398 prompt = "failed. try again with nostr address / bunker uri / nsec"; 477 uri
399 } 478 } else {
479 prompt = "failed. try again with nostr address / bunker uri / nsec";
480 continue;
481 }
482 } else {
483 prompt = "invalid. try again with nostr address / bunker uri / nsec";
484 continue;
485 };
486 match get_nip46_signer_from_uri_and_key(&uri.to_string(), &app_key_secret).await {
487 Ok(signer) => {
488 let pub_key = fetch_public_key(&signer).await?;
489 if let Err(error) = save_bunker(
490 git_repo,
491 &pub_key,
492 &uri.to_string(),
493 &app_key_secret,
494 always_save,
495 ) {
496 eprintln!("{error}");
497 }
498 public_key = Some(pub_key);
499 break signer;
500 }
501 Err(_) => {
502 prompt = "failed. try again with nostr address / bunker uri / nsec";
503 }
504 }
505 };
506 let public_key = if let Some(public_key) = public_key {
507 public_key
508 } else {
509 signer.public_key().await?
510 };
511 (signer, public_key)
400 } 512 }
401 }; 513 };
402 let public_key = if let Some(public_key) = public_key {
403 public_key
404 } else {
405 signer.public_key().await?
406 };
407 // lookup profile 514 // lookup profile
408 let user_ref = get_user_details(&public_key, client, git_repo.get_path()?, false).await?; 515 let user_ref = get_user_details(&public_key, client, git_repo.get_path()?, false).await?;
409 print_logged_in_as(&user_ref, client.is_none())?; 516 print_logged_in_as(&user_ref, client.is_none())?;
410 Ok((signer, user_ref)) 517 Ok((signer, user_ref))
411} 518}
412 519
520fn generate_qr(data: &str) -> Result<Vec<String>> {
521 let mut lines = vec![];
522 let qr =
523 QrCode::new(data.as_bytes()).context("failed to create QR of nostrconnect login url")?;
524 let colors = qr.to_colors();
525 let rows: Vec<&[qrcode::Color]> = colors.chunks(qr.width()).collect();
526 for (row, data) in rows.iter().enumerate() {
527 let odd = row % 2 != 0;
528 if odd {
529 continue;
530 }
531 let mut line = String::new();
532 for (col, color) in data.iter().enumerate() {
533 let top = color;
534 let mut bottom = qrcode::Color::Light;
535 if let Some(next_row_data) = rows.get(row + 1) {
536 if let Some(color) = next_row_data.get(col) {
537 bottom = *color;
538 }
539 }
540 line.push(if *top == qrcode::Color::Dark {
541 if bottom == qrcode::Color::Dark {
542 '█'
543 } else {
544 '▀'
545 }
546 } else if bottom == qrcode::Color::Dark {
547 '▄'
548 } else {
549 ' '
550 });
551 }
552 lines.push(line);
553 }
554 Ok(lines)
555}
556
413pub async fn fetch_nip46_uri_from_nip05(nip05: &str) -> Result<NostrConnectURI> { 557pub async fn fetch_nip46_uri_from_nip05(nip05: &str) -> Result<NostrConnectURI> {
414 let term = console::Term::stderr(); 558 let term = console::Term::stderr();
415 term.write_line("contacting login service provider...")?; 559 term.write_line("contacting login service provider...")?;
@@ -447,7 +591,7 @@ fn save_bunker(
447 { 591 {
448 let global = !Interactor::default().confirm( 592 let global = !Interactor::default().confirm(
449 PromptConfirmParms::default() 593 PromptConfirmParms::default()
450 .with_prompt("just for this repository?") 594 .with_prompt("save login just for this repository?")
451 .with_default(false), 595 .with_default(false),
452 )?; 596 )?;
453 let npub = public_key.to_bech32()?; 597 let npub = public_key.to_bech32()?;
@@ -523,6 +667,7 @@ fn save_keys(git_repo: &Repo, keys: &nostr::Keys, always_save: bool) -> Result<(
523 save_to_git_config(git_repo, &npub, &Some(nsec_string.clone()), &None, false)?; 667 save_to_git_config(git_repo, &npub, &Some(nsec_string.clone()), &None, false)?;
524 } 668 }
525 } else { 669 } else {
670 eprintln!("{error}");
526 Err(error)?; 671 Err(error)?;
527 } 672 }
528 }; 673 };