diff options
| author | DanConwayDev <DanConwayDev@protonmail.com> | 2024-09-04 08:04:48 +0100 |
|---|---|---|
| committer | DanConwayDev <DanConwayDev@protonmail.com> | 2024-09-04 13:30:59 +0100 |
| commit | 949c6459aa7683453a7160423b689ceadb08954b (patch) | |
| tree | 230c26ecb11b99916e5570e548673eb09ecf0a36 /src/login.rs | |
| parent | a825311f2c55661aaab3a163bda9109295c96044 (diff) | |
refactor: organise into lib and bin structure
the make the code more readable
this commit just moves the files, the next commit should fix the imports
Diffstat (limited to 'src/login.rs')
| -rw-r--r-- | src/login.rs | 695 |
1 files changed, 0 insertions, 695 deletions
diff --git a/src/login.rs b/src/login.rs deleted file mode 100644 index 19bb97c..0000000 --- a/src/login.rs +++ /dev/null | |||
| @@ -1,695 +0,0 @@ | |||
| 1 | use std::{collections::HashSet, path::Path, str::FromStr, time::Duration}; | ||
| 2 | |||
| 3 | use anyhow::{bail, Context, Result}; | ||
| 4 | use nostr::{ | ||
| 5 | nips::{nip05, nip46::NostrConnectURI}, | ||
| 6 | PublicKey, | ||
| 7 | }; | ||
| 8 | use nostr_sdk::{ | ||
| 9 | Alphabet, FromBech32, JsonUtil, Keys, Kind, NostrSigner, SingleLetterTag, Timestamp, ToBech32, | ||
| 10 | }; | ||
| 11 | use nostr_signer::Nip46Signer; | ||
| 12 | |||
| 13 | #[cfg(not(test))] | ||
| 14 | use crate::client::Client; | ||
| 15 | #[cfg(test)] | ||
| 16 | use crate::client::MockConnect; | ||
| 17 | use crate::{ | ||
| 18 | cli_interactor::{ | ||
| 19 | Interactor, InteractorPrompt, PromptConfirmParms, PromptInputParms, PromptPasswordParms, | ||
| 20 | }, | ||
| 21 | client::{fetch_public_key, get_event_from_global_cache, Connect}, | ||
| 22 | config::{UserMetadata, UserRef, UserRelayRef, UserRelays}, | ||
| 23 | git::{Repo, RepoActions}, | ||
| 24 | key_handling::encryption::{decrypt_key, encrypt_key}, | ||
| 25 | }; | ||
| 26 | |||
| 27 | /// handles the encrpytion and storage of key material | ||
| 28 | #[allow(clippy::too_many_arguments)] | ||
| 29 | pub async fn launch( | ||
| 30 | git_repo: &Repo, | ||
| 31 | bunker_uri: &Option<String>, | ||
| 32 | bunker_app_key: &Option<String>, | ||
| 33 | nsec: &Option<String>, | ||
| 34 | password: &Option<String>, | ||
| 35 | #[cfg(test)] client: Option<&MockConnect>, | ||
| 36 | #[cfg(not(test))] client: Option<&Client>, | ||
| 37 | change_user: bool, | ||
| 38 | silent: bool, | ||
| 39 | ) -> Result<(NostrSigner, UserRef)> { | ||
| 40 | if let Ok(signer) = match get_signer_without_prompts( | ||
| 41 | git_repo, | ||
| 42 | bunker_uri, | ||
| 43 | bunker_app_key, | ||
| 44 | nsec, | ||
| 45 | password, | ||
| 46 | change_user, | ||
| 47 | ) | ||
| 48 | .await | ||
| 49 | { | ||
| 50 | Ok(signer) => Ok(signer), | ||
| 51 | Err(error) => { | ||
| 52 | if error | ||
| 53 | .to_string() | ||
| 54 | .eq("git config item nostr.nsec is an ncryptsec") | ||
| 55 | { | ||
| 56 | println!( | ||
| 57 | "login as {}", | ||
| 58 | if let Ok(public_key) = PublicKey::from_bech32( | ||
| 59 | get_config_item(git_repo, "nostr.npub") | ||
| 60 | .unwrap_or("unknown ncryptsec".to_string()), | ||
| 61 | ) { | ||
| 62 | if let Ok(user_ref) = | ||
| 63 | get_user_details(&public_key, client, git_repo.get_path()?, silent) | ||
| 64 | .await | ||
| 65 | { | ||
| 66 | user_ref.metadata.name | ||
| 67 | } else { | ||
| 68 | "unknown ncryptsec".to_string() | ||
| 69 | } | ||
| 70 | } else { | ||
| 71 | "unknown ncryptsec".to_string() | ||
| 72 | } | ||
| 73 | ); | ||
| 74 | loop { | ||
| 75 | // prompt for password | ||
| 76 | let password = Interactor::default() | ||
| 77 | .password(PromptPasswordParms::default().with_prompt("password")) | ||
| 78 | .context("failed to get password input from interactor.password")?; | ||
| 79 | if let Ok(keys) = get_keys_with_password(git_repo, &password) { | ||
| 80 | break Ok(NostrSigner::Keys(keys)); | ||
| 81 | } | ||
| 82 | println!("incorrect password"); | ||
| 83 | } | ||
| 84 | } else { | ||
| 85 | if nsec.is_some() { | ||
| 86 | bail!(error); | ||
| 87 | } | ||
| 88 | Err(error) | ||
| 89 | } | ||
| 90 | } | ||
| 91 | } { | ||
| 92 | // get user ref | ||
| 93 | let user_ref = get_user_details( | ||
| 94 | &signer | ||
| 95 | .public_key() | ||
| 96 | .await | ||
| 97 | .context("cannot get public key from signer")?, | ||
| 98 | client, | ||
| 99 | git_repo.get_path()?, | ||
| 100 | silent, | ||
| 101 | ) | ||
| 102 | .await?; | ||
| 103 | if !silent { | ||
| 104 | print_logged_in_as(&user_ref, client.is_none())?; | ||
| 105 | } | ||
| 106 | Ok((signer, user_ref)) | ||
| 107 | } else if silent { | ||
| 108 | bail!("TODO: enable interactive login in nostr git remote helper"); | ||
| 109 | } else { | ||
| 110 | fresh_login(git_repo, client, change_user).await | ||
| 111 | } | ||
| 112 | } | ||
| 113 | |||
| 114 | fn print_logged_in_as(user_ref: &UserRef, offline_mode: bool) -> Result<()> { | ||
| 115 | if !offline_mode && user_ref.metadata.created_at.eq(&Timestamp::from(0)) { | ||
| 116 | println!("cannot find profile..."); | ||
| 117 | } else if !offline_mode && user_ref.metadata.name.eq(&user_ref.public_key.to_bech32()?) { | ||
| 118 | println!("cannot extract account name from account metadata..."); | ||
| 119 | } else if !offline_mode && user_ref.relays.created_at.eq(&Timestamp::from(0)) { | ||
| 120 | println!( | ||
| 121 | "cannot find your relay list. consider using another nostr client to create one to enhance your nostr experience." | ||
| 122 | ); | ||
| 123 | } | ||
| 124 | println!("logged in as {}", user_ref.metadata.name); | ||
| 125 | Ok(()) | ||
| 126 | } | ||
| 127 | |||
| 128 | async fn get_signer_without_prompts( | ||
| 129 | git_repo: &Repo, | ||
| 130 | bunker_uri: &Option<String>, | ||
| 131 | bunker_app_key: &Option<String>, | ||
| 132 | nsec: &Option<String>, | ||
| 133 | password: &Option<String>, | ||
| 134 | save_local: bool, | ||
| 135 | ) -> Result<NostrSigner> { | ||
| 136 | if let Some(nsec) = nsec { | ||
| 137 | Ok(NostrSigner::Keys(get_keys_from_nsec( | ||
| 138 | git_repo, nsec, password, save_local, | ||
| 139 | )?)) | ||
| 140 | } else if let Some(password) = password { | ||
| 141 | Ok(NostrSigner::Keys(get_keys_with_password( | ||
| 142 | git_repo, password, | ||
| 143 | )?)) | ||
| 144 | } else if let Some(bunker_uri) = bunker_uri { | ||
| 145 | if let Some(bunker_app_key) = bunker_app_key { | ||
| 146 | let signer = get_nip46_signer_from_uri_and_key(bunker_uri, bunker_app_key) | ||
| 147 | .await | ||
| 148 | .context("failed to connect with remote signer")?; | ||
| 149 | if save_local { | ||
| 150 | save_to_git_config( | ||
| 151 | git_repo, | ||
| 152 | &signer.public_key().await?.to_bech32()?, | ||
| 153 | &None, | ||
| 154 | &Some((bunker_uri.to_string(),bunker_app_key.to_string())), | ||
| 155 | false, | ||
| 156 | ) | ||
| 157 | .context("failed to save bunker details local git config nostr.bunker-uri and nostr.bunker-app-key")?; | ||
| 158 | } | ||
| 159 | Ok(signer) | ||
| 160 | } else { | ||
| 161 | bail!( | ||
| 162 | "bunker-app-key parameter must be provided alongside bunker-uri. if unknown, login interactively." | ||
| 163 | ) | ||
| 164 | } | ||
| 165 | } else if !save_local { | ||
| 166 | get_signer_with_git_config_nsec_or_bunker_without_prompts(git_repo).await | ||
| 167 | } else { | ||
| 168 | bail!("user wants prompts to specify new keys") | ||
| 169 | } | ||
| 170 | } | ||
| 171 | |||
| 172 | fn get_keys_from_nsec( | ||
| 173 | git_repo: &Repo, | ||
| 174 | nsec: &String, | ||
| 175 | password: &Option<String>, | ||
| 176 | save_local: bool, | ||
| 177 | ) -> Result<nostr::Keys> { | ||
| 178 | #[allow(unused_assignments)] | ||
| 179 | let mut s = String::new(); | ||
| 180 | let keys = if nsec.contains("ncryptsec") { | ||
| 181 | s = nsec.to_string(); | ||
| 182 | decrypt_key( | ||
| 183 | nsec, | ||
| 184 | password | ||
| 185 | .clone() | ||
| 186 | .context("password must be supplied when using ncryptsec as nsec parameter")? | ||
| 187 | .as_str(), | ||
| 188 | ) | ||
| 189 | .context("failed to decrypt key with provided password") | ||
| 190 | .context("failed to decrypt ncryptsec supplied as nsec with password")? | ||
| 191 | } else { | ||
| 192 | s = nsec.to_string(); | ||
| 193 | nostr::Keys::from_str(nsec).context("invalid nsec parameter")? | ||
| 194 | }; | ||
| 195 | if save_local { | ||
| 196 | if let Some(password) = password { | ||
| 197 | s = encrypt_key(&keys, password)?; | ||
| 198 | } | ||
| 199 | save_to_git_config( | ||
| 200 | git_repo, | ||
| 201 | &keys.public_key().to_bech32()?, | ||
| 202 | &Some(s), | ||
| 203 | &None, | ||
| 204 | false, | ||
| 205 | ) | ||
| 206 | .context("failed to save encrypted nsec in local git config nostr.nsec")?; | ||
| 207 | } | ||
| 208 | Ok(keys) | ||
| 209 | } | ||
| 210 | |||
| 211 | fn save_to_git_config( | ||
| 212 | git_repo: &Repo, | ||
| 213 | npub: &str, | ||
| 214 | nsec: &Option<String>, | ||
| 215 | bunker: &Option<(String, String)>, | ||
| 216 | global: bool, | ||
| 217 | ) -> Result<()> { | ||
| 218 | if let Err(error) = silently_save_to_git_config(git_repo, npub, nsec, bunker, global) { | ||
| 219 | println!( | ||
| 220 | "failed to save login details to {} git config", | ||
| 221 | if global { "global" } else { "local" } | ||
| 222 | ); | ||
| 223 | if let Some(nsec) = nsec { | ||
| 224 | if nsec.contains("ncryptsec") { | ||
| 225 | println!("manually set git config nostr.nsec to: {nsec}"); | ||
| 226 | } else { | ||
| 227 | println!("manually set git config nostr.nsec"); | ||
| 228 | } | ||
| 229 | } | ||
| 230 | if let Some(bunker) = bunker { | ||
| 231 | println!("manually set git config as follows:"); | ||
| 232 | println!("nostr.bunker-uri: {}", bunker.0); | ||
| 233 | println!("nostr.bunker-app-key: {}", bunker.1); | ||
| 234 | } | ||
| 235 | Err(error) | ||
| 236 | } else { | ||
| 237 | println!( | ||
| 238 | "saved login details to {} git config", | ||
| 239 | if global { "global" } else { "local" } | ||
| 240 | ); | ||
| 241 | Ok(()) | ||
| 242 | } | ||
| 243 | } | ||
| 244 | fn silently_save_to_git_config( | ||
| 245 | git_repo: &Repo, | ||
| 246 | npub: &str, | ||
| 247 | nsec: &Option<String>, | ||
| 248 | bunker: &Option<(String, String)>, | ||
| 249 | global: bool, | ||
| 250 | ) -> Result<()> { | ||
| 251 | // must do this first otherwise it might remove the global items just added | ||
| 252 | if global { | ||
| 253 | git_repo.remove_git_config_item("nostr.npub", false)?; | ||
| 254 | git_repo.remove_git_config_item("nostr.nsec", false)?; | ||
| 255 | git_repo.remove_git_config_item("nostr.bunker-uri", false)?; | ||
| 256 | git_repo.remove_git_config_item("nostr.bunker-app-key", false)?; | ||
| 257 | } | ||
| 258 | if let Some(bunker) = bunker { | ||
| 259 | git_repo.remove_git_config_item("nostr.nsec", global)?; | ||
| 260 | git_repo.save_git_config_item("nostr.bunker-uri", &bunker.0, global)?; | ||
| 261 | git_repo.save_git_config_item("nostr.bunker-app-key", &bunker.1, global)?; | ||
| 262 | } | ||
| 263 | if let Some(nsec) = nsec { | ||
| 264 | git_repo.save_git_config_item("nostr.nsec", nsec, global)?; | ||
| 265 | git_repo.remove_git_config_item("nostr.bunker-uri", global)?; | ||
| 266 | git_repo.remove_git_config_item("nostr.bunker-app-key", global)?; | ||
| 267 | } | ||
| 268 | git_repo.save_git_config_item("nostr.npub", npub, global) | ||
| 269 | } | ||
| 270 | |||
| 271 | fn get_keys_with_password(git_repo: &Repo, password: &str) -> Result<nostr::Keys> { | ||
| 272 | decrypt_key( | ||
| 273 | &git_repo | ||
| 274 | .get_git_config_item("nostr.nsec", None) | ||
| 275 | .context("failed get git config")? | ||
| 276 | .context("git config item nostr.nsec doesn't exist so cannot decrypt it")?, | ||
| 277 | password, | ||
| 278 | ) | ||
| 279 | .context("failed to decrypt stored nsec key with provided password") | ||
| 280 | } | ||
| 281 | |||
| 282 | async fn get_nip46_signer_from_uri_and_key(uri: &str, app_key: &str) -> Result<NostrSigner> { | ||
| 283 | let term = console::Term::stderr(); | ||
| 284 | term.write_line("connecting to remote signer...")?; | ||
| 285 | let uri = NostrConnectURI::parse(uri)?; | ||
| 286 | let signer = NostrSigner::nip46( | ||
| 287 | Nip46Signer::new( | ||
| 288 | uri, | ||
| 289 | nostr::Keys::from_str(app_key).context("invalid app key")?, | ||
| 290 | Duration::from_secs(30), | ||
| 291 | None, | ||
| 292 | ) | ||
| 293 | .await?, | ||
| 294 | ); | ||
| 295 | term.clear_last_lines(1)?; | ||
| 296 | Ok(signer) | ||
| 297 | } | ||
| 298 | |||
| 299 | async fn get_signer_with_git_config_nsec_or_bunker_without_prompts( | ||
| 300 | git_repo: &Repo, | ||
| 301 | ) -> Result<NostrSigner> { | ||
| 302 | if let Ok(local_nsec) = &git_repo | ||
| 303 | .get_git_config_item("nostr.nsec", Some(false)) | ||
| 304 | .context("failed get local git config")? | ||
| 305 | .context("git local config item nostr.nsec doesn't exist") | ||
| 306 | { | ||
| 307 | if local_nsec.contains("ncryptsec") { | ||
| 308 | bail!("git global config item nostr.nsec is an ncryptsec") | ||
| 309 | } | ||
| 310 | Ok(NostrSigner::Keys( | ||
| 311 | nostr::Keys::from_str(local_nsec).context("invalid nsec parameter")?, | ||
| 312 | )) | ||
| 313 | } else if let Ok((uri, app_key)) = get_git_config_bunker_uri_and_app_key(git_repo, Some(false)) | ||
| 314 | { | ||
| 315 | get_nip46_signer_from_uri_and_key(&uri, &app_key).await | ||
| 316 | } else if let Ok(global_nsec) = &git_repo | ||
| 317 | .get_git_config_item("nostr.nsec", Some(true)) | ||
| 318 | .context("failed get global git config")? | ||
| 319 | .context("git global config item nostr.nsec doesn't exist") | ||
| 320 | { | ||
| 321 | if global_nsec.contains("ncryptsec") { | ||
| 322 | bail!("git global config item nostr.nsec is an ncryptsec") | ||
| 323 | } | ||
| 324 | Ok(NostrSigner::Keys( | ||
| 325 | nostr::Keys::from_str(global_nsec).context("invalid nsec parameter")?, | ||
| 326 | )) | ||
| 327 | } else if let Ok((uri, app_key)) = get_git_config_bunker_uri_and_app_key(git_repo, Some(true)) { | ||
| 328 | get_nip46_signer_from_uri_and_key(&uri, &app_key).await | ||
| 329 | } else { | ||
| 330 | bail!("cannot get nsec or bunker from git config") | ||
| 331 | } | ||
| 332 | } | ||
| 333 | |||
| 334 | fn get_git_config_bunker_uri_and_app_key( | ||
| 335 | git_repo: &Repo, | ||
| 336 | global: Option<bool>, | ||
| 337 | ) -> Result<(String, String)> { | ||
| 338 | Ok(( | ||
| 339 | git_repo | ||
| 340 | .get_git_config_item("nostr.bunker-uri", global) | ||
| 341 | .context("failed get local git config")? | ||
| 342 | .context("git local config item nostr.bunker-uri doesn't exist")? | ||
| 343 | .to_string(), | ||
| 344 | git_repo | ||
| 345 | .get_git_config_item("nostr.bunker-app-key", global) | ||
| 346 | .context("failed get local git config")? | ||
| 347 | .context("git local config item nostr.bunker-app-key doesn't exist")? | ||
| 348 | .to_string(), | ||
| 349 | )) | ||
| 350 | } | ||
| 351 | |||
| 352 | async fn fresh_login( | ||
| 353 | git_repo: &Repo, | ||
| 354 | #[cfg(test)] client: Option<&MockConnect>, | ||
| 355 | #[cfg(not(test))] client: Option<&Client>, | ||
| 356 | always_save: bool, | ||
| 357 | ) -> Result<(NostrSigner, UserRef)> { | ||
| 358 | let mut public_key: Option<PublicKey> = None; | ||
| 359 | // prompt for nsec | ||
| 360 | let mut prompt = "login with nostr address / nsec"; | ||
| 361 | let signer = loop { | ||
| 362 | let input = Interactor::default() | ||
| 363 | .input(PromptInputParms::default().with_prompt(prompt)) | ||
| 364 | .context("failed to get nsec input from interactor")?; | ||
| 365 | if let Ok(keys) = nostr::Keys::from_str(&input) { | ||
| 366 | if let Err(error) = save_keys(git_repo, &keys, always_save) { | ||
| 367 | println!("{error}"); | ||
| 368 | } | ||
| 369 | break NostrSigner::Keys(keys); | ||
| 370 | } | ||
| 371 | let uri = if let Ok(uri) = NostrConnectURI::parse(&input) { | ||
| 372 | uri | ||
| 373 | } else if input.contains('@') { | ||
| 374 | if let Ok(uri) = fetch_nip46_uri_from_nip05(&input).await { | ||
| 375 | uri | ||
| 376 | } else { | ||
| 377 | prompt = "failed. try again with nostr address / bunker uri / nsec"; | ||
| 378 | continue; | ||
| 379 | } | ||
| 380 | } else { | ||
| 381 | prompt = "invalid. try again with nostr address / bunker uri / nsec"; | ||
| 382 | continue; | ||
| 383 | }; | ||
| 384 | let app_key = Keys::generate().secret_key()?.to_secret_hex(); | ||
| 385 | match get_nip46_signer_from_uri_and_key(&uri.to_string(), &app_key).await { | ||
| 386 | Ok(signer) => { | ||
| 387 | let pub_key = fetch_public_key(&signer).await?; | ||
| 388 | if let Err(error) = | ||
| 389 | save_bunker(git_repo, &pub_key, &uri.to_string(), &app_key, always_save) | ||
| 390 | { | ||
| 391 | println!("{error}"); | ||
| 392 | } | ||
| 393 | public_key = Some(pub_key); | ||
| 394 | break signer; | ||
| 395 | } | ||
| 396 | Err(_) => { | ||
| 397 | prompt = "failed. try again with nostr address / bunker uri / nsec"; | ||
| 398 | } | ||
| 399 | } | ||
| 400 | }; | ||
| 401 | let public_key = if let Some(public_key) = public_key { | ||
| 402 | public_key | ||
| 403 | } else { | ||
| 404 | signer.public_key().await? | ||
| 405 | }; | ||
| 406 | // lookup profile | ||
| 407 | let user_ref = get_user_details(&public_key, client, git_repo.get_path()?, false).await?; | ||
| 408 | print_logged_in_as(&user_ref, client.is_none())?; | ||
| 409 | Ok((signer, user_ref)) | ||
| 410 | } | ||
| 411 | |||
| 412 | pub async fn fetch_nip46_uri_from_nip05(nip05: &str) -> Result<NostrConnectURI> { | ||
| 413 | let term = console::Term::stderr(); | ||
| 414 | term.write_line("contacting login service provider...")?; | ||
| 415 | let res = nip05::profile(&nip05, None).await; | ||
| 416 | term.clear_last_lines(1)?; | ||
| 417 | match res { | ||
| 418 | Ok(profile) => { | ||
| 419 | if profile.nip46.is_empty() { | ||
| 420 | println!("nip05 provider isn't configured for remote login"); | ||
| 421 | bail!("nip05 provider isn't configured for remote login") | ||
| 422 | } | ||
| 423 | Ok(NostrConnectURI::Bunker { | ||
| 424 | signer_public_key: profile.public_key, | ||
| 425 | relays: profile.nip46, | ||
| 426 | secret: None, | ||
| 427 | }) | ||
| 428 | } | ||
| 429 | Err(error) => { | ||
| 430 | println!("error contacting login service provider: {error}"); | ||
| 431 | Err(error).context("error contacting login service provider") | ||
| 432 | } | ||
| 433 | } | ||
| 434 | } | ||
| 435 | |||
| 436 | fn save_bunker( | ||
| 437 | git_repo: &Repo, | ||
| 438 | public_key: &PublicKey, | ||
| 439 | uri: &str, | ||
| 440 | app_key: &str, | ||
| 441 | always_save: bool, | ||
| 442 | ) -> Result<()> { | ||
| 443 | if always_save | ||
| 444 | || Interactor::default() | ||
| 445 | .confirm(PromptConfirmParms::default().with_prompt("save login details?"))? | ||
| 446 | { | ||
| 447 | let global = !Interactor::default().confirm( | ||
| 448 | PromptConfirmParms::default() | ||
| 449 | .with_prompt("just for this repository?") | ||
| 450 | .with_default(false), | ||
| 451 | )?; | ||
| 452 | let npub = public_key.to_bech32()?; | ||
| 453 | if let Err(error) = save_to_git_config( | ||
| 454 | git_repo, | ||
| 455 | &npub, | ||
| 456 | &None, | ||
| 457 | &Some((uri.to_string(), app_key.to_string())), | ||
| 458 | global, | ||
| 459 | ) { | ||
| 460 | if global { | ||
| 461 | if Interactor::default().confirm( | ||
| 462 | PromptConfirmParms::default() | ||
| 463 | .with_prompt("save in repository git config?") | ||
| 464 | .with_default(true), | ||
| 465 | )? { | ||
| 466 | save_to_git_config( | ||
| 467 | git_repo, | ||
| 468 | &npub, | ||
| 469 | &None, | ||
| 470 | &Some((uri.to_string(), app_key.to_string())), | ||
| 471 | false, | ||
| 472 | )?; | ||
| 473 | } | ||
| 474 | } else { | ||
| 475 | Err(error)?; | ||
| 476 | } | ||
| 477 | }; | ||
| 478 | } | ||
| 479 | Ok(()) | ||
| 480 | } | ||
| 481 | |||
| 482 | fn save_keys(git_repo: &Repo, keys: &nostr::Keys, always_save: bool) -> Result<()> { | ||
| 483 | if always_save | ||
| 484 | || Interactor::default() | ||
| 485 | .confirm(PromptConfirmParms::default().with_prompt("save login details?"))? | ||
| 486 | { | ||
| 487 | let global = !Interactor::default().confirm( | ||
| 488 | PromptConfirmParms::default() | ||
| 489 | .with_prompt("just for this repository?") | ||
| 490 | .with_default(false), | ||
| 491 | )?; | ||
| 492 | |||
| 493 | let encrypt = Interactor::default().confirm( | ||
| 494 | PromptConfirmParms::default() | ||
| 495 | .with_prompt("require password?") | ||
| 496 | .with_default(false), | ||
| 497 | )?; | ||
| 498 | |||
| 499 | let npub = keys.public_key().to_bech32()?; | ||
| 500 | let nsec_string = if encrypt { | ||
| 501 | let password = Interactor::default() | ||
| 502 | .password( | ||
| 503 | PromptPasswordParms::default() | ||
| 504 | .with_prompt("encrypt with password") | ||
| 505 | .with_confirm(), | ||
| 506 | ) | ||
| 507 | .context("failed to get password input from interactor.password")?; | ||
| 508 | encrypt_key(keys, &password)? | ||
| 509 | } else { | ||
| 510 | keys.secret_key()?.to_bech32()? | ||
| 511 | }; | ||
| 512 | |||
| 513 | if let Err(error) = | ||
| 514 | save_to_git_config(git_repo, &npub, &Some(nsec_string.clone()), &None, global) | ||
| 515 | { | ||
| 516 | if global { | ||
| 517 | if Interactor::default().confirm( | ||
| 518 | PromptConfirmParms::default() | ||
| 519 | .with_prompt("save in repository git config?") | ||
| 520 | .with_default(true), | ||
| 521 | )? { | ||
| 522 | save_to_git_config(git_repo, &npub, &Some(nsec_string.clone()), &None, false)?; | ||
| 523 | } | ||
| 524 | } else { | ||
| 525 | Err(error)?; | ||
| 526 | } | ||
| 527 | }; | ||
| 528 | }; | ||
| 529 | Ok(()) | ||
| 530 | } | ||
| 531 | |||
| 532 | fn get_config_item(git_repo: &Repo, name: &str) -> Result<String> { | ||
| 533 | git_repo | ||
| 534 | .get_git_config_item(name, None) | ||
| 535 | .context("failed get git config")? | ||
| 536 | .context(format!("git config item {name} doesn't exist")) | ||
| 537 | } | ||
| 538 | |||
| 539 | fn extract_user_metadata( | ||
| 540 | public_key: &nostr::PublicKey, | ||
| 541 | events: &[nostr::Event], | ||
| 542 | ) -> Result<UserMetadata> { | ||
| 543 | let event = events | ||
| 544 | .iter() | ||
| 545 | .filter(|e| e.kind.eq(&nostr::Kind::Metadata) && e.pubkey.eq(public_key)) | ||
| 546 | .max_by_key(|e| e.created_at); | ||
| 547 | |||
| 548 | let metadata: Option<nostr::Metadata> = if let Some(event) = event { | ||
| 549 | Some( | ||
| 550 | nostr::Metadata::from_json(event.content.clone()) | ||
| 551 | .context("metadata cannot be found in kind 0 event content")?, | ||
| 552 | ) | ||
| 553 | } else { | ||
| 554 | None | ||
| 555 | }; | ||
| 556 | |||
| 557 | Ok(UserMetadata { | ||
| 558 | name: if let Some(metadata) = metadata { | ||
| 559 | if let Some(n) = metadata.name { | ||
| 560 | n | ||
| 561 | } else if let Some(n) = metadata.custom.get("displayName") { | ||
| 562 | // strip quote marks that custom.get() adds | ||
| 563 | let binding = n.to_string(); | ||
| 564 | let mut chars = binding.chars(); | ||
| 565 | chars.next(); | ||
| 566 | chars.next_back(); | ||
| 567 | chars.as_str().to_string() | ||
| 568 | } else if let Some(n) = metadata.display_name { | ||
| 569 | n | ||
| 570 | } else { | ||
| 571 | public_key.to_bech32()? | ||
| 572 | } | ||
| 573 | } else { | ||
| 574 | public_key.to_bech32()? | ||
| 575 | }, | ||
| 576 | created_at: if let Some(event) = event { | ||
| 577 | event.created_at | ||
| 578 | } else { | ||
| 579 | Timestamp::from(0) | ||
| 580 | }, | ||
| 581 | }) | ||
| 582 | } | ||
| 583 | |||
| 584 | fn extract_user_relays(public_key: &nostr::PublicKey, events: &[nostr::Event]) -> UserRelays { | ||
| 585 | let event = events | ||
| 586 | .iter() | ||
| 587 | .filter(|e| e.kind.eq(&nostr::Kind::RelayList) && e.pubkey.eq(public_key)) | ||
| 588 | .max_by_key(|e| e.created_at); | ||
| 589 | |||
| 590 | UserRelays { | ||
| 591 | relays: if let Some(event) = event { | ||
| 592 | event | ||
| 593 | .tags | ||
| 594 | .iter() | ||
| 595 | .filter(|t| { | ||
| 596 | t.kind() | ||
| 597 | .eq(&nostr::TagKind::SingleLetter(SingleLetterTag::lowercase( | ||
| 598 | Alphabet::R, | ||
| 599 | ))) | ||
| 600 | }) | ||
| 601 | .map(|t| UserRelayRef { | ||
| 602 | url: t.as_vec()[1].clone(), | ||
| 603 | read: t.as_vec().len() == 2 || t.as_vec()[2].eq("read"), | ||
| 604 | write: t.as_vec().len() == 2 || t.as_vec()[2].eq("write"), | ||
| 605 | }) | ||
| 606 | .collect() | ||
| 607 | } else { | ||
| 608 | vec![] | ||
| 609 | }, | ||
| 610 | created_at: if let Some(event) = event { | ||
| 611 | event.created_at | ||
| 612 | } else { | ||
| 613 | Timestamp::from(0) | ||
| 614 | }, | ||
| 615 | } | ||
| 616 | } | ||
| 617 | |||
| 618 | async fn get_user_details( | ||
| 619 | public_key: &PublicKey, | ||
| 620 | #[cfg(test)] client: Option<&crate::client::MockConnect>, | ||
| 621 | #[cfg(not(test))] client: Option<&Client>, | ||
| 622 | git_repo_path: &Path, | ||
| 623 | cache_only: bool, | ||
| 624 | ) -> Result<UserRef> { | ||
| 625 | if let Ok(user_ref) = get_user_ref_from_cache(git_repo_path, public_key).await { | ||
| 626 | Ok(user_ref) | ||
| 627 | } else { | ||
| 628 | let empty = UserRef { | ||
| 629 | public_key: public_key.to_owned(), | ||
| 630 | metadata: extract_user_metadata(public_key, &[])?, | ||
| 631 | relays: extract_user_relays(public_key, &[]), | ||
| 632 | }; | ||
| 633 | if cache_only { | ||
| 634 | Ok(empty) | ||
| 635 | } else if let Some(client) = client { | ||
| 636 | let term = console::Term::stderr(); | ||
| 637 | term.write_line("searching for profile...")?; | ||
| 638 | let (_, progress_reporter) = client | ||
| 639 | .fetch_all( | ||
| 640 | git_repo_path, | ||
| 641 | &HashSet::new(), | ||
| 642 | &HashSet::from_iter(vec![*public_key]), | ||
| 643 | ) | ||
| 644 | .await?; | ||
| 645 | if let Ok(user_ref) = get_user_ref_from_cache(git_repo_path, public_key).await { | ||
| 646 | progress_reporter.clear()?; | ||
| 647 | // if std::env::var("NGITTEST").is_err() {term.clear_last_lines(1)?;} | ||
| 648 | Ok(user_ref) | ||
| 649 | } else { | ||
| 650 | Ok(empty) | ||
| 651 | } | ||
| 652 | } else { | ||
| 653 | Ok(empty) | ||
| 654 | } | ||
| 655 | } | ||
| 656 | } | ||
| 657 | pub async fn get_logged_in_user(git_repo_path: &Path) -> Result<Option<PublicKey>> { | ||
| 658 | let git_repo = Repo::from_path(&git_repo_path.to_path_buf())?; | ||
| 659 | Ok( | ||
| 660 | if let Some(npub) = git_repo.get_git_config_item("nostr.npub", None)? { | ||
| 661 | if let Ok(pubic_key) = PublicKey::parse(npub) { | ||
| 662 | Some(pubic_key) | ||
| 663 | } else { | ||
| 664 | None | ||
| 665 | } | ||
| 666 | } else { | ||
| 667 | None | ||
| 668 | }, | ||
| 669 | ) | ||
| 670 | } | ||
| 671 | |||
| 672 | pub async fn get_user_ref_from_cache( | ||
| 673 | git_repo_path: &Path, | ||
| 674 | public_key: &PublicKey, | ||
| 675 | ) -> Result<UserRef> { | ||
| 676 | let filters = vec![ | ||
| 677 | nostr::Filter::default() | ||
| 678 | .author(*public_key) | ||
| 679 | .kind(Kind::Metadata), | ||
| 680 | nostr::Filter::default() | ||
| 681 | .author(*public_key) | ||
| 682 | .kind(Kind::RelayList), | ||
| 683 | ]; | ||
| 684 | |||
| 685 | let events = get_event_from_global_cache(git_repo_path, filters.clone()).await?; | ||
| 686 | |||
| 687 | if events.is_empty() { | ||
| 688 | bail!("no metadata and profile list in cache for selected public key"); | ||
| 689 | } | ||
| 690 | Ok(UserRef { | ||
| 691 | public_key: public_key.to_owned(), | ||
| 692 | metadata: extract_user_metadata(public_key, &events)?, | ||
| 693 | relays: extract_user_relays(public_key, &events), | ||
| 694 | }) | ||
| 695 | } | ||