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.rs31
-rw-r--r--src/git.rs93
-rw-r--r--src/login.rs374
-rw-r--r--src/main.rs7
-rw-r--r--src/repo_ref.rs14
-rw-r--r--src/sub_commands/init.rs2
-rw-r--r--src/sub_commands/login.rs22
-rw-r--r--src/sub_commands/push.rs2
-rw-r--r--src/sub_commands/send.rs12
9 files changed, 465 insertions, 92 deletions
diff --git a/src/client.rs b/src/client.rs
index 9dba528..44abb29 100644
--- a/src/client.rs
+++ b/src/client.rs
@@ -19,7 +19,7 @@ use indicatif::{MultiProgress, ProgressBar, ProgressState, ProgressStyle};
19#[cfg(test)] 19#[cfg(test)]
20use mockall::*; 20use mockall::*;
21use nostr::Event; 21use nostr::Event;
22use nostr_sdk::NostrSigner; 22use nostr_sdk::{EventBuilder, NostrSigner};
23 23
24#[allow(clippy::struct_field_names)] 24#[allow(clippy::struct_field_names)]
25pub struct Client { 25pub struct Client {
@@ -292,3 +292,32 @@ fn get_dedup_events(relay_results: Vec<Result<Vec<nostr::Event>>>) -> Vec<Event>
292 } 292 }
293 dedup_events 293 dedup_events
294} 294}
295
296pub async fn sign_event(event_builder: EventBuilder, signer: &NostrSigner) -> Result<nostr::Event> {
297 if signer.r#type().eq(&nostr_signer::NostrSignerType::NIP46) {
298 let term = console::Term::stderr();
299 term.write_line("signing event with remote signer...")?;
300 let event = signer
301 .sign_event_builder(event_builder)
302 .await
303 .context("failed to sign event")?;
304 term.clear_last_lines(1)?;
305 Ok(event)
306 } else {
307 signer
308 .sign_event_builder(event_builder)
309 .await
310 .context("failed to sign event")
311 }
312}
313
314pub async fn fetch_public_key(signer: &NostrSigner) -> Result<nostr::PublicKey> {
315 let term = console::Term::stderr();
316 term.write_line("fetching npub from remote signer...")?;
317 let public_key = signer
318 .public_key()
319 .await
320 .context("failed to get npub from remote signer")?;
321 term.clear_last_lines(1)?;
322 Ok(public_key)
323}
diff --git a/src/git.rs b/src/git.rs
index 46687ae..bb943a9 100644
--- a/src/git.rs
+++ b/src/git.rs
@@ -76,8 +76,9 @@ pub trait RepoActions {
76 ) -> Result<Vec<nostr::Event>>; 76 ) -> Result<Vec<nostr::Event>>;
77 fn parse_starting_commits(&self, starting_commits: &str) -> Result<Vec<Sha1Hash>>; 77 fn parse_starting_commits(&self, starting_commits: &str) -> Result<Vec<Sha1Hash>>;
78 fn ancestor_of(&self, decendant: &Sha1Hash, ancestor: &Sha1Hash) -> Result<bool>; 78 fn ancestor_of(&self, decendant: &Sha1Hash, ancestor: &Sha1Hash) -> Result<bool>;
79 fn get_git_config_item(&self, item: &str, global: bool) -> Result<Option<String>>; 79 fn get_git_config_item(&self, item: &str, global: Option<bool>) -> Result<Option<String>>;
80 fn save_git_config_item(&self, item: &str, value: &str, global: bool) -> Result<()>; 80 fn save_git_config_item(&self, item: &str, value: &str, global: bool) -> Result<()>;
81 fn remove_git_config_item(&self, item: &str, global: bool) -> Result<bool>;
81} 82}
82 83
83impl RepoActions for Repo { 84impl RepoActions for Repo {
@@ -581,8 +582,15 @@ impl RepoActions for Repo {
581 } 582 }
582 } 583 }
583 584
584 fn get_git_config_item(&self, item: &str, global: bool) -> Result<Option<String>> { 585 /// setting global to None will suppliment local config with global items
585 match if global { 586 /// not in local
587 fn get_git_config_item(&self, item: &str, global: Option<bool>) -> Result<Option<String>> {
588 let just_global = if let Some(just_global) = global {
589 just_global
590 } else {
591 false
592 };
593 match if just_global {
586 self.git_repo 594 self.git_repo
587 .config() 595 .config()
588 .context("cannot open git config")? 596 .context("cannot open git config")?
@@ -593,11 +601,22 @@ impl RepoActions for Repo {
593 } 601 }
594 .get_entry(item) 602 .get_entry(item)
595 { 603 {
596 Ok(item) => Ok(Some( 604 Ok(item) => {
597 item.value() 605 if let Some(global) = global {
598 .context("cannot find git config item")? 606 if item.level().eq(&git2::ConfigLevel::Local) {
599 .to_string(), 607 if global {
600 )), 608 bail!("only local repository login available")
609 }
610 } else if !global {
611 bail!("only global repository login available")
612 }
613 }
614 Ok(Some(
615 item.value()
616 .context("cannot find git config item")?
617 .to_string(),
618 ))
619 }
601 Err(_) => Ok(None), 620 Err(_) => Ok(None),
602 } 621 }
603 } 622 }
@@ -613,9 +632,33 @@ impl RepoActions for Repo {
613 self.git_repo.config().context("cannot open git config")? 632 self.git_repo.config().context("cannot open git config")?
614 } 633 }
615 .set_str(item, value) 634 .set_str(item, value)
616 .context("cannot set git config value")?; 635 .context(format!(
636 "cannot set {} git config item {}",
637 if global { "global" } else { "local" },
638 item
639 ))?;
617 Ok(()) 640 Ok(())
618 } 641 }
642
643 /// returns false if item doesn't exist
644 fn remove_git_config_item(&self, item: &str, global: bool) -> Result<bool> {
645 if self.get_git_config_item(item, Some(global))?.is_none() {
646 Ok(false)
647 } else {
648 if global {
649 self.git_repo
650 .config()
651 .context("cannot open git config")?
652 .open_global()
653 .context("cannot open global git config")?
654 } else {
655 self.git_repo.config().context("cannot open git config")?
656 }
657 .remove(item)
658 .context("cannot remove existing git config item")?;
659 Ok(true)
660 }
661 }
619} 662}
620 663
621fn oid_to_u8_20_bytes(oid: &Oid) -> [u8; 20] { 664fn oid_to_u8_20_bytes(oid: &Oid) -> [u8; 20] {
@@ -849,7 +892,9 @@ mod tests {
849 let git_repo = Repo::from_path(&test_repo.dir)?; 892 let git_repo = Repo::from_path(&test_repo.dir)?;
850 git_repo.save_git_config_item("test.item", "testvalue", false)?; 893 git_repo.save_git_config_item("test.item", "testvalue", false)?;
851 assert_eq!( 894 assert_eq!(
852 git_repo.get_git_config_item("test.item", false)?.unwrap(), 895 git_repo
896 .get_git_config_item("test.item", Some(false))?
897 .unwrap(),
853 "testvalue", 898 "testvalue",
854 ); 899 );
855 Ok(()) 900 Ok(())
@@ -859,7 +904,10 @@ mod tests {
859 fn get_git_config_item_returns_none_if_not_present() -> Result<()> { 904 fn get_git_config_item_returns_none_if_not_present() -> Result<()> {
860 let test_repo = GitTestRepo::default(); 905 let test_repo = GitTestRepo::default();
861 let git_repo = Repo::from_path(&test_repo.dir)?; 906 let git_repo = Repo::from_path(&test_repo.dir)?;
862 assert_eq!(git_repo.get_git_config_item("test.item", false)?, None); 907 assert_eq!(
908 git_repo.get_git_config_item("test.item", Some(false))?,
909 None
910 );
863 Ok(()) 911 Ok(())
864 } 912 }
865 913
@@ -869,11 +917,32 @@ mod tests {
869 let git_repo = Repo::from_path(&test_repo.dir)?; 917 let git_repo = Repo::from_path(&test_repo.dir)?;
870 git_repo.save_git_config_item("test.item", "", false)?; 918 git_repo.save_git_config_item("test.item", "", false)?;
871 assert_eq!( 919 assert_eq!(
872 git_repo.get_git_config_item("test.item", false)?, 920 git_repo.get_git_config_item("test.item", Some(false))?,
873 Some("".to_string()), 921 Some("".to_string()),
874 ); 922 );
875 Ok(()) 923 Ok(())
876 } 924 }
925
926 #[test]
927 fn remove_local_git_config_item() -> Result<()> {
928 let test_repo = GitTestRepo::default();
929 let git_repo = Repo::from_path(&test_repo.dir)?;
930 git_repo.save_git_config_item("test.item", "testvalue", false)?;
931 assert!(git_repo.remove_git_config_item("test.item", false)?);
932 assert_eq!(
933 git_repo.get_git_config_item("test.item", Some(false))?,
934 None,
935 );
936 Ok(())
937 }
938
939 #[test]
940 fn remove_git_config_item_returns_false_if_item_wasnt_set() -> Result<()> {
941 let test_repo = GitTestRepo::default();
942 let git_repo = Repo::from_path(&test_repo.dir)?;
943 assert!(!(git_repo.remove_git_config_item("test.item", false)?));
944 Ok(())
945 }
877 } 946 }
878 947
879 #[test] 948 #[test]
diff --git a/src/login.rs b/src/login.rs
index e1669c1..218a079 100644
--- a/src/login.rs
+++ b/src/login.rs
@@ -1,11 +1,13 @@
1use std::str::FromStr; 1use std::{fs::create_dir_all, str::FromStr, time::Duration};
2 2
3use anyhow::{bail, Context, Result}; 3use anyhow::{bail, Context, Result};
4use nostr::PublicKey; 4use nostr::{nips::nip46::NostrConnectURI, PublicKey};
5use nostr_database::Order; 5use nostr_database::Order;
6use nostr_sdk::{ 6use nostr_sdk::{
7 Alphabet, FromBech32, JsonUtil, Kind, NostrDatabase, NostrSigner, SingleLetterTag, ToBech32, 7 Alphabet, FromBech32, JsonUtil, Keys, Kind, NostrDatabase, NostrSigner, SingleLetterTag,
8 ToBech32,
8}; 9};
10use nostr_signer::Nip46Signer;
9use nostr_sqlite::SQLiteDatabase; 11use nostr_sqlite::SQLiteDatabase;
10 12
11#[cfg(not(test))] 13#[cfg(not(test))]
@@ -16,7 +18,7 @@ use crate::{
16 cli_interactor::{ 18 cli_interactor::{
17 Interactor, InteractorPrompt, PromptConfirmParms, PromptInputParms, PromptPasswordParms, 19 Interactor, InteractorPrompt, PromptConfirmParms, PromptInputParms, PromptPasswordParms,
18 }, 20 },
19 client::Connect, 21 client::{fetch_public_key, Connect},
20 config::{get_dirs, UserMetadata, UserRef, UserRelayRef, UserRelays}, 22 config::{get_dirs, UserMetadata, UserRef, UserRelayRef, UserRelays},
21 git::{Repo, RepoActions}, 23 git::{Repo, RepoActions},
22 key_handling::encryption::{decrypt_key, encrypt_key}, 24 key_handling::encryption::{decrypt_key, encrypt_key},
@@ -25,14 +27,25 @@ use crate::{
25/// handles the encrpytion and storage of key material 27/// handles the encrpytion and storage of key material
26pub async fn launch( 28pub async fn launch(
27 git_repo: &Repo, 29 git_repo: &Repo,
30 bunker_uri: &Option<String>,
31 bunker_app_key: &Option<String>,
28 nsec: &Option<String>, 32 nsec: &Option<String>,
29 password: &Option<String>, 33 password: &Option<String>,
30 #[cfg(test)] client: Option<&MockConnect>, 34 #[cfg(test)] client: Option<&MockConnect>,
31 #[cfg(not(test))] client: Option<&Client>, 35 #[cfg(not(test))] client: Option<&Client>,
32 change_user: bool, 36 change_user: bool,
33) -> Result<(NostrSigner, UserRef)> { 37) -> Result<(NostrSigner, UserRef)> {
34 if let Ok(keys) = match get_keys_without_prompts(git_repo, nsec, password, change_user) { 38 if let Ok(signer) = match get_signer_without_prompts(
35 Ok(keys) => Ok(keys), 39 git_repo,
40 bunker_uri,
41 bunker_app_key,
42 nsec,
43 password,
44 change_user,
45 )
46 .await
47 {
48 Ok(signer) => Ok(signer),
36 Err(error) => { 49 Err(error) => {
37 if error 50 if error
38 .to_string() 51 .to_string()
@@ -60,7 +73,7 @@ pub async fn launch(
60 .password(PromptPasswordParms::default().with_prompt("password")) 73 .password(PromptPasswordParms::default().with_prompt("password"))
61 .context("failed to get password input from interactor.password")?; 74 .context("failed to get password input from interactor.password")?;
62 if let Ok(keys) = get_keys_with_password(git_repo, &password) { 75 if let Ok(keys) = get_keys_with_password(git_repo, &password) {
63 break Ok(keys); 76 break Ok(NostrSigner::Keys(keys));
64 } 77 }
65 println!("incorrect password"); 78 println!("incorrect password");
66 } 79 }
@@ -73,9 +86,17 @@ pub async fn launch(
73 } 86 }
74 } { 87 } {
75 // get user ref 88 // get user ref
76 let user_ref = get_user_details(&keys.public_key(), client, git_repo).await?; 89 let user_ref = get_user_details(
90 &signer
91 .public_key()
92 .await
93 .context("cannot get public key from signer")?,
94 client,
95 git_repo,
96 )
97 .await?;
77 print_logged_in_as(&user_ref, client.is_none())?; 98 print_logged_in_as(&user_ref, client.is_none())?;
78 Ok((NostrSigner::Keys(keys), user_ref)) 99 Ok((signer, user_ref))
79 } else { 100 } else {
80 fresh_login(git_repo, client, change_user).await 101 fresh_login(git_repo, client, change_user).await
81 } 102 }
@@ -95,18 +116,45 @@ fn print_logged_in_as(user_ref: &UserRef, offline_mode: bool) -> Result<()> {
95 Ok(()) 116 Ok(())
96} 117}
97 118
98fn get_keys_without_prompts( 119async fn get_signer_without_prompts(
99 git_repo: &Repo, 120 git_repo: &Repo,
121 bunker_uri: &Option<String>,
122 bunker_app_key: &Option<String>,
100 nsec: &Option<String>, 123 nsec: &Option<String>,
101 password: &Option<String>, 124 password: &Option<String>,
102 save_local: bool, 125 save_local: bool,
103) -> Result<nostr::Keys> { 126) -> Result<NostrSigner> {
104 if let Some(nsec) = nsec { 127 if let Some(nsec) = nsec {
105 get_keys_from_nsec(git_repo, nsec, password, save_local) 128 Ok(NostrSigner::Keys(get_keys_from_nsec(
129 git_repo, nsec, password, save_local,
130 )?))
106 } else if let Some(password) = password { 131 } else if let Some(password) = password {
107 get_keys_with_password(git_repo, password) 132 Ok(NostrSigner::Keys(get_keys_with_password(
133 git_repo, password,
134 )?))
135 } else if let Some(bunker_uri) = bunker_uri {
136 if let Some(bunker_app_key) = bunker_app_key {
137 let signer = get_nip46_signer_from_uri_and_key(bunker_uri, bunker_app_key)
138 .await
139 .context("failed to connect with remote signer")?;
140 if save_local {
141 save_to_git_config(
142 git_repo,
143 &signer.public_key().await?.to_bech32()?,
144 &None,
145 &Some((bunker_uri.to_string(),bunker_app_key.to_string())),
146 false,
147 )
148 .context("failed to save bunker details local git config nostr.bunker-uri and nostr.bunker-app-key")?;
149 }
150 Ok(signer)
151 } else {
152 bail!(
153 "bunker-app-key parameter must be provided alongside bunker-uri. if unknown, login interactively."
154 )
155 }
108 } else if !save_local { 156 } else if !save_local {
109 get_keys_with_git_config_nsec_without_prompts(git_repo) 157 get_signer_with_git_config_nsec_or_bunker_without_prompts(git_repo).await
110 } else { 158 } else {
111 bail!("user wants prompts to specify new keys") 159 bail!("user wants prompts to specify new keys")
112 } 160 }
@@ -139,18 +187,82 @@ fn get_keys_from_nsec(
139 if let Some(password) = password { 187 if let Some(password) = password {
140 s = encrypt_key(&keys, password)?; 188 s = encrypt_key(&keys, password)?;
141 } 189 }
142 git_repo 190 save_to_git_config(
143 .save_git_config_item("nostr.nsec", &s, false) 191 git_repo,
144 .context("failed to save encrypted nsec in local git config nostr.nsec")?; 192 &keys.public_key().to_bech32()?,
145 git_repo.save_git_config_item("nostr.npub", &keys.public_key().to_bech32()?, false)?; 193 &Some(s),
194 &None,
195 false,
196 )
197 .context("failed to save encrypted nsec in local git config nostr.nsec")?;
146 } 198 }
147 Ok(keys) 199 Ok(keys)
148} 200}
149 201
202fn save_to_git_config(
203 git_repo: &Repo,
204 npub: &str,
205 nsec: &Option<String>,
206 bunker: &Option<(String, String)>,
207 global: bool,
208) -> Result<()> {
209 if let Err(error) = silently_save_to_git_config(git_repo, npub, nsec, bunker, global) {
210 println!(
211 "failed to save login details to {} git config",
212 if global { "global" } else { "local" }
213 );
214 if let Some(nsec) = nsec {
215 if nsec.contains("ncryptsec") {
216 println!("manually set git config nostr.nsec to: {nsec}");
217 } else {
218 println!("manually set git config nostr.nsec");
219 }
220 }
221 if let Some(bunker) = bunker {
222 println!("manually set git config as follows:");
223 println!("nostr.bunker-uri: {}", bunker.0);
224 println!("nostr.bunker-app-key: {}", bunker.1);
225 }
226 Err(error)
227 } else {
228 println!(
229 "saved login details to {} git config",
230 if global { "global" } else { "local" }
231 );
232 Ok(())
233 }
234}
235fn silently_save_to_git_config(
236 git_repo: &Repo,
237 npub: &str,
238 nsec: &Option<String>,
239 bunker: &Option<(String, String)>,
240 global: bool,
241) -> Result<()> {
242 // must do this first otherwise it might remove the global items just added
243 if global {
244 git_repo.remove_git_config_item("nostr.npub", false)?;
245 git_repo.remove_git_config_item("nostr.nsec", false)?;
246 git_repo.remove_git_config_item("nostr.bunker-uri", false)?;
247 git_repo.remove_git_config_item("nostr.bunker-app-key", false)?;
248 }
249 if let Some(bunker) = bunker {
250 git_repo.remove_git_config_item("nostr.nsec", global)?;
251 git_repo.save_git_config_item("nostr.bunker-uri", &bunker.0, global)?;
252 git_repo.save_git_config_item("nostr.bunker-app-key", &bunker.1, global)?;
253 }
254 if let Some(nsec) = nsec {
255 git_repo.save_git_config_item("nostr.nsec", nsec, global)?;
256 git_repo.remove_git_config_item("nostr.bunker-uri", global)?;
257 git_repo.remove_git_config_item("nostr.bunker-app-key", global)?;
258 }
259 git_repo.save_git_config_item("nostr.npub", npub, global)
260}
261
150fn get_keys_with_password(git_repo: &Repo, password: &str) -> Result<nostr::Keys> { 262fn get_keys_with_password(git_repo: &Repo, password: &str) -> Result<nostr::Keys> {
151 decrypt_key( 263 decrypt_key(
152 &git_repo 264 &git_repo
153 .get_git_config_item("nostr.nsec", false) 265 .get_git_config_item("nostr.nsec", None)
154 .context("failed get git config")? 266 .context("failed get git config")?
155 .context("git config item nostr.nsec doesn't exist so cannot decrypt it")?, 267 .context("git config item nostr.nsec doesn't exist so cannot decrypt it")?,
156 password, 268 password,
@@ -158,15 +270,74 @@ fn get_keys_with_password(git_repo: &Repo, password: &str) -> Result<nostr::Keys
158 .context("failed to decrypt stored nsec key with provided password") 270 .context("failed to decrypt stored nsec key with provided password")
159} 271}
160 272
161fn get_keys_with_git_config_nsec_without_prompts(git_repo: &Repo) -> Result<nostr::Keys> { 273async fn get_nip46_signer_from_uri_and_key(uri: &str, app_key: &str) -> Result<NostrSigner> {
162 let nsec = &git_repo 274 let term = console::Term::stderr();
163 .get_git_config_item("nostr.nsec", false) 275 term.write_line("connecting to remote signer...")?;
164 .context("failed get git config")? 276 let uri = NostrConnectURI::parse(uri)?;
165 .context("git config item nostr.nsec doesn't exist")?; 277 let signer = NostrSigner::nip46(
166 if nsec.contains("ncryptsec") { 278 Nip46Signer::new(
167 bail!("git config item nostr.nsec is an ncryptsec") 279 uri,
280 nostr::Keys::from_str(app_key).context("invalid app key")?,
281 Duration::from_secs(30),
282 None,
283 )
284 .await?,
285 );
286 term.clear_last_lines(1)?;
287 Ok(signer)
288}
289
290async fn get_signer_with_git_config_nsec_or_bunker_without_prompts(
291 git_repo: &Repo,
292) -> Result<NostrSigner> {
293 if let Ok(local_nsec) = &git_repo
294 .get_git_config_item("nostr.nsec", Some(false))
295 .context("failed get local git config")?
296 .context("git local config item nostr.nsec doesn't exist")
297 {
298 if local_nsec.contains("ncryptsec") {
299 bail!("git global config item nostr.nsec is an ncryptsec")
300 }
301 Ok(NostrSigner::Keys(
302 nostr::Keys::from_str(local_nsec).context("invalid nsec parameter")?,
303 ))
304 } else if let Ok((uri, app_key)) = get_git_config_bunker_uri_and_app_key(git_repo, Some(false))
305 {
306 get_nip46_signer_from_uri_and_key(&uri, &app_key).await
307 } else if let Ok(global_nsec) = &git_repo
308 .get_git_config_item("nostr.nsec", Some(true))
309 .context("failed get global git config")?
310 .context("git global config item nostr.nsec doesn't exist")
311 {
312 if global_nsec.contains("ncryptsec") {
313 bail!("git global config item nostr.nsec is an ncryptsec")
314 }
315 Ok(NostrSigner::Keys(
316 nostr::Keys::from_str(global_nsec).context("invalid nsec parameter")?,
317 ))
318 } else if let Ok((uri, app_key)) = get_git_config_bunker_uri_and_app_key(git_repo, Some(true)) {
319 get_nip46_signer_from_uri_and_key(&uri, &app_key).await
320 } else {
321 bail!("cannot get nsec or bunker from git config")
168 } 322 }
169 nostr::Keys::from_str(nsec).context("invalid nsec parameter") 323}
324
325fn get_git_config_bunker_uri_and_app_key(
326 git_repo: &Repo,
327 global: Option<bool>,
328) -> Result<(String, String)> {
329 Ok((
330 git_repo
331 .get_git_config_item("nostr.bunker_url", global)
332 .context("failed get local git config")?
333 .context("git local config item nostr.bunker_url doesn't exist")?
334 .to_string(),
335 git_repo
336 .get_git_config_item("nostr.bunker-app-key", global)
337 .context("failed get local git config")?
338 .context("git local config item nostr.bunker-app-key doesn't exist")?
339 .to_string(),
340 ))
170} 341}
171 342
172async fn fresh_login( 343async fn fresh_login(
@@ -175,50 +346,119 @@ async fn fresh_login(
175 #[cfg(not(test))] client: Option<&Client>, 346 #[cfg(not(test))] client: Option<&Client>,
176 always_save: bool, 347 always_save: bool,
177) -> Result<(NostrSigner, UserRef)> { 348) -> Result<(NostrSigner, UserRef)> {
349 let mut public_key: Option<PublicKey> = None;
178 // prompt for nsec 350 // prompt for nsec
179 let mut prompt = "login with nsec"; 351 let mut prompt = "login with bunker uri / nsec";
180 let keys = loop { 352 let signer = loop {
181 match nostr::Keys::from_str( 353 let input = Interactor::default()
182 &Interactor::default() 354 .input(PromptInputParms::default().with_prompt(prompt))
183 .input(PromptInputParms::default().with_prompt(prompt)) 355 .context("failed to get nsec input from interactor")?;
184 .context("failed to get nsec input from interactor")?, 356 match nostr::Keys::from_str(&input) {
185 ) {
186 Ok(key) => { 357 Ok(key) => {
187 break key; 358 if let Err(error) = save_keys(git_repo, &key, always_save) {
188 } 359 println!("{error}");
189 Err(_) => { 360 }
190 prompt = "invalid nsec. try again with nsec (or hex private key)"; 361 break NostrSigner::Keys(key);
191 } 362 }
363 Err(_) => match NostrConnectURI::parse(&input) {
364 Ok(_) => {
365 let app_key = Keys::generate().secret_key()?.to_secret_hex();
366 match get_nip46_signer_from_uri_and_key(&input, &app_key).await {
367 Ok(signer) => {
368 let pub_key = fetch_public_key(&signer).await?;
369 if let Err(error) =
370 save_bunker(git_repo, &pub_key, &input, &app_key, always_save)
371 {
372 println!("{error}");
373 }
374 public_key = Some(pub_key);
375 break signer;
376 }
377 Err(_) => {
378 prompt = "invalid. try again with nostr address / nsec";
379 }
380 }
381 }
382 Err(_) => {
383 prompt = "invalid. try again with nostr address / nsec";
384 }
385 },
192 } 386 }
193 }; 387 };
388 let public_key = if let Some(public_key) = public_key {
389 public_key
390 } else {
391 signer.public_key().await?
392 };
194 // lookup profile 393 // lookup profile
195 // save keys 394 let user_ref = get_user_details(&public_key, client, git_repo).await?;
196 if let Err(error) = save_keys(git_repo, &keys, always_save) {
197 println!("{error}");
198 }
199 let user_ref = get_user_details(&keys.public_key(), client, git_repo).await?;
200 print_logged_in_as(&user_ref, client.is_none())?; 395 print_logged_in_as(&user_ref, client.is_none())?;
201 Ok((NostrSigner::Keys(keys), user_ref)) 396 Ok((signer, user_ref))
202} 397}
203 398
204fn save_keys(git_repo: &Repo, keys: &nostr::Keys, always_save: bool) -> Result<()> { 399fn save_bunker(
205 let store = always_save 400 git_repo: &Repo,
401 public_key: &PublicKey,
402 uri: &str,
403 app_key: &str,
404 always_save: bool,
405) -> Result<()> {
406 if always_save
206 || Interactor::default() 407 || Interactor::default()
207 .confirm(PromptConfirmParms::default().with_prompt("save login details?"))?; 408 .confirm(PromptConfirmParms::default().with_prompt("save login details?"))?
409 {
410 let global = !Interactor::default().confirm(
411 PromptConfirmParms::default()
412 .with_prompt("just for this repository?")
413 .with_default(false),
414 )?;
415 let npub = public_key.to_bech32()?;
416 if let Err(error) = save_to_git_config(
417 git_repo,
418 &npub,
419 &None,
420 &Some((uri.to_string(), app_key.to_string())),
421 global,
422 ) {
423 if global {
424 if Interactor::default().confirm(
425 PromptConfirmParms::default()
426 .with_prompt("save in repository git config?")
427 .with_default(true),
428 )? {
429 save_to_git_config(
430 git_repo,
431 &npub,
432 &None,
433 &Some((uri.to_string(), app_key.to_string())),
434 false,
435 )?;
436 }
437 } else {
438 Err(error)?;
439 }
440 };
441 }
442 Ok(())
443}
208 444
209 let global = !Interactor::default().confirm( 445fn save_keys(git_repo: &Repo, keys: &nostr::Keys, always_save: bool) -> Result<()> {
210 PromptConfirmParms::default() 446 if always_save
211 .with_prompt("just for this repository?") 447 || Interactor::default()
212 .with_default(false), 448 .confirm(PromptConfirmParms::default().with_prompt("save login details?"))?
213 )?; 449 {
450 let global = !Interactor::default().confirm(
451 PromptConfirmParms::default()
452 .with_prompt("just for this repository?")
453 .with_default(false),
454 )?;
214 455
215 let encrypt = Interactor::default().confirm( 456 let encrypt = Interactor::default().confirm(
216 PromptConfirmParms::default() 457 PromptConfirmParms::default()
217 .with_prompt("require password?") 458 .with_prompt("require password?")
218 .with_default(false), 459 .with_default(false),
219 )?; 460 )?;
220 461
221 if store {
222 let npub = keys.public_key().to_bech32()?; 462 let npub = keys.public_key().to_bech32()?;
223 let nsec_string = if encrypt { 463 let nsec_string = if encrypt {
224 let password = Interactor::default() 464 let password = Interactor::default()
@@ -233,22 +473,20 @@ fn save_keys(git_repo: &Repo, keys: &nostr::Keys, always_save: bool) -> Result<(
233 keys.secret_key()?.to_bech32()? 473 keys.secret_key()?.to_bech32()?
234 }; 474 };
235 475
236 if let Err(error) = git_repo.save_git_config_item("nostr.nsec", &nsec_string, global) { 476 if let Err(error) =
477 save_to_git_config(git_repo, &npub, &Some(nsec_string.clone()), &None, global)
478 {
237 if global { 479 if global {
238 println!("failed to edit global git config instead");
239 if Interactor::default().confirm( 480 if Interactor::default().confirm(
240 PromptConfirmParms::default() 481 PromptConfirmParms::default()
241 .with_prompt("save in repository git config?") 482 .with_prompt("save in repository git config?")
242 .with_default(true), 483 .with_default(true),
243 )? { 484 )? {
244 git_repo.save_git_config_item("nostr.nsec", &nsec_string, false)?; 485 save_to_git_config(git_repo, &npub, &Some(nsec_string.clone()), &None, false)?;
245 git_repo.save_git_config_item("nostr.npub", &npub, false)?;
246 } 486 }
247 } else { 487 } else {
248 bail!(error) 488 Err(error)?;
249 } 489 }
250 } else {
251 git_repo.save_git_config_item("nostr.npub", &npub, global)?;
252 }; 490 };
253 }; 491 };
254 Ok(()) 492 Ok(())
@@ -256,7 +494,7 @@ fn save_keys(git_repo: &Repo, keys: &nostr::Keys, always_save: bool) -> Result<(
256 494
257fn get_config_item(git_repo: &Repo, name: &str) -> Result<String> { 495fn get_config_item(git_repo: &Repo, name: &str) -> Result<String> {
258 git_repo 496 git_repo
259 .get_git_config_item(name, false) 497 .get_git_config_item(name, None)
260 .context("failed get git config")? 498 .context("failed get git config")?
261 .context(format!("git config item {name} doesn't exist")) 499 .context(format!("git config item {name} doesn't exist"))
262} 500}
@@ -350,6 +588,10 @@ async fn get_user_details(
350 println!("searching for profile and relay updates..."); 588 println!("searching for profile and relay updates...");
351 } 589 }
352 let database = SQLiteDatabase::open(if std::env::var("NGITTEST").is_err() { 590 let database = SQLiteDatabase::open(if std::env::var("NGITTEST").is_err() {
591 create_dir_all(get_dirs()?.config_dir()).context(format!(
592 "cannot create cache directory in: {:?}",
593 get_dirs()?.config_dir()
594 ))?;
353 get_dirs()?.config_dir().join("cache.sqlite") 595 get_dirs()?.config_dir().join("cache.sqlite")
354 } else { 596 } else {
355 git_repo.get_path()?.join(".git/test-global-cache.sqlite") 597 git_repo.get_path()?.join(".git/test-global-cache.sqlite")
diff --git a/src/main.rs b/src/main.rs
index 30ecea3..9f53084 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,4 +1,5 @@
1#![cfg_attr(not(test), warn(clippy::pedantic))] 1#![cfg_attr(not(test), warn(clippy::pedantic))]
2#![allow(clippy::large_futures)]
2#![cfg_attr(not(test), warn(clippy::expect_used))] 3#![cfg_attr(not(test), warn(clippy::expect_used))]
3 4
4use anyhow::Result; 5use anyhow::Result;
@@ -19,6 +20,12 @@ mod sub_commands;
19pub struct Cli { 20pub struct Cli {
20 #[command(subcommand)] 21 #[command(subcommand)]
21 command: Commands, 22 command: Commands,
23 /// remote signer address
24 #[arg(long, global = true)]
25 bunker_uri: Option<String>,
26 /// remote signer app secret key
27 #[arg(long, global = true)]
28 bunker_app_key: Option<String>,
22 /// nsec or hex private key 29 /// nsec or hex private key
23 #[arg(short, long, global = true)] 30 #[arg(short, long, global = true)]
24 nsec: Option<String>, 31 nsec: Option<String>,
diff --git a/src/repo_ref.rs b/src/repo_ref.rs
index 2b0d024..426640f 100644
--- a/src/repo_ref.rs
+++ b/src/repo_ref.rs
@@ -11,7 +11,7 @@ use crate::client::Client;
11use crate::client::MockConnect; 11use crate::client::MockConnect;
12use crate::{ 12use crate::{
13 cli_interactor::{Interactor, InteractorPrompt, PromptInputParms}, 13 cli_interactor::{Interactor, InteractorPrompt, PromptInputParms},
14 client::Connect, 14 client::{sign_event, Connect},
15 git::{Repo, RepoActions}, 15 git::{Repo, RepoActions},
16}; 16};
17 17
@@ -95,8 +95,8 @@ pub static REPO_REF_KIND: u16 = 30_617;
95 95
96impl RepoRef { 96impl RepoRef {
97 pub async fn to_event(&self, signer: &NostrSigner) -> Result<nostr::Event> { 97 pub async fn to_event(&self, signer: &NostrSigner) -> Result<nostr::Event> {
98 signer 98 sign_event(
99 .sign_event_builder(nostr_sdk::EventBuilder::new( 99 nostr_sdk::EventBuilder::new(
100 nostr::event::Kind::Custom(REPO_REF_KIND), 100 nostr::event::Kind::Custom(REPO_REF_KIND),
101 "", 101 "",
102 [ 102 [
@@ -152,9 +152,11 @@ impl RepoRef {
152 // code languages and hashtags 152 // code languages and hashtags
153 ] 153 ]
154 .concat(), 154 .concat(),
155 )) 155 ),
156 .await 156 signer,
157 .context("failed to create repository reference event") 157 )
158 .await
159 .context("failed to create repository reference event")
158 } 160 }
159} 161}
160 162
diff --git a/src/sub_commands/init.rs b/src/sub_commands/init.rs
index 4afe83c..57785db 100644
--- a/src/sub_commands/init.rs
+++ b/src/sub_commands/init.rs
@@ -61,6 +61,8 @@ pub async fn launch(cli_args: &Cli, args: &SubCommandArgs) -> Result<()> {
61 61
62 let (signer, user_ref) = login::launch( 62 let (signer, user_ref) = login::launch(
63 &git_repo, 63 &git_repo,
64 &cli_args.bunker_uri,
65 &cli_args.bunker_app_key,
64 &cli_args.nsec, 66 &cli_args.nsec,
65 &cli_args.password, 67 &cli_args.password,
66 Some(&client), 68 Some(&client),
diff --git a/src/sub_commands/login.rs b/src/sub_commands/login.rs
index e71d431..6f49ba8 100644
--- a/src/sub_commands/login.rs
+++ b/src/sub_commands/login.rs
@@ -17,7 +17,16 @@ pub struct SubCommandArgs {
17pub async fn launch(args: &Cli, command_args: &SubCommandArgs) -> Result<()> { 17pub async fn launch(args: &Cli, command_args: &SubCommandArgs) -> Result<()> {
18 let git_repo = Repo::discover().context("cannot find a git repository")?; 18 let git_repo = Repo::discover().context("cannot find a git repository")?;
19 if command_args.offline { 19 if command_args.offline {
20 login::launch(&git_repo, &args.nsec, &args.password, None, true).await?; 20 login::launch(
21 &git_repo,
22 &args.bunker_uri,
23 &args.bunker_app_key,
24 &args.nsec,
25 &args.password,
26 None,
27 true,
28 )
29 .await?;
21 Ok(()) 30 Ok(())
22 } else { 31 } else {
23 #[cfg(not(test))] 32 #[cfg(not(test))]
@@ -25,7 +34,16 @@ pub async fn launch(args: &Cli, command_args: &SubCommandArgs) -> Result<()> {
25 #[cfg(test)] 34 #[cfg(test)]
26 let client = <MockConnect as std::default::Default>::default(); 35 let client = <MockConnect as std::default::Default>::default();
27 36
28 login::launch(&git_repo, &args.nsec, &args.password, Some(&client), true).await?; 37 login::launch(
38 &git_repo,
39 &args.bunker_uri,
40 &args.bunker_app_key,
41 &args.nsec,
42 &args.password,
43 Some(&client),
44 true,
45 )
46 .await?;
29 client.disconnect().await?; 47 client.disconnect().await?;
30 Ok(()) 48 Ok(())
31 } 49 }
diff --git a/src/sub_commands/push.rs b/src/sub_commands/push.rs
index 92c1c18..3c471c0 100644
--- a/src/sub_commands/push.rs
+++ b/src/sub_commands/push.rs
@@ -150,6 +150,8 @@ pub async fn launch(cli_args: &Cli, args: &SubCommandArgs) -> Result<()> {
150 150
151 let (signer, user_ref) = login::launch( 151 let (signer, user_ref) = login::launch(
152 &git_repo, 152 &git_repo,
153 &cli_args.bunker_uri,
154 &cli_args.bunker_app_key,
153 &cli_args.nsec, 155 &cli_args.nsec,
154 &cli_args.password, 156 &cli_args.password,
155 Some(&client), 157 Some(&client),
diff --git a/src/sub_commands/send.rs b/src/sub_commands/send.rs
index 1d20e90..7c8f2ee 100644
--- a/src/sub_commands/send.rs
+++ b/src/sub_commands/send.rs
@@ -19,7 +19,7 @@ use crate::{
19 cli_interactor::{ 19 cli_interactor::{
20 Interactor, InteractorPrompt, PromptConfirmParms, PromptInputParms, PromptMultiChoiceParms, 20 Interactor, InteractorPrompt, PromptConfirmParms, PromptInputParms, PromptMultiChoiceParms,
21 }, 21 },
22 client::Connect, 22 client::{sign_event, Connect},
23 git::{Repo, RepoActions}, 23 git::{Repo, RepoActions},
24 login, 24 login,
25 repo_ref::{self, RepoRef, REPO_REF_KIND}, 25 repo_ref::{self, RepoRef, REPO_REF_KIND},
@@ -180,6 +180,8 @@ pub async fn launch(cli_args: &Cli, args: &SubCommandArgs) -> Result<()> {
180 }; 180 };
181 let (signer, user_ref) = login::launch( 181 let (signer, user_ref) = login::launch(
182 &git_repo, 182 &git_repo,
183 &cli_args.bunker_uri,
184 &cli_args.bunker_app_key,
183 &cli_args.nsec, 185 &cli_args.nsec,
184 &cli_args.password, 186 &cli_args.password,
185 Some(&client), 187 Some(&client),
@@ -593,7 +595,7 @@ pub async fn generate_cover_letter_and_patch_events(
593 let mut events = vec![]; 595 let mut events = vec![];
594 596
595 if let Some((title, description)) = cover_letter_title_description { 597 if let Some((title, description)) = cover_letter_title_description {
596 events.push(signer.sign_event_builder(EventBuilder::new( 598 events.push(sign_event(EventBuilder::new(
597 nostr::event::Kind::Custom(PATCH_KIND), 599 nostr::event::Kind::Custom(PATCH_KIND),
598 format!( 600 format!(
599 "From {} Mon Sep 17 00:00:00 2001\nSubject: [PATCH 0/{}] {title}\n\n{description}", 601 "From {} Mon Sep 17 00:00:00 2001\nSubject: [PATCH 0/{}] {title}\n\n{description}",
@@ -656,7 +658,7 @@ pub async fn generate_cover_letter_and_patch_events(
656 .map(|pk| Tag::public_key(*pk)) 658 .map(|pk| Tag::public_key(*pk))
657 .collect(), 659 .collect(),
658 ].concat(), 660 ].concat(),
659 )).await 661 ), signer).await
660 .context("failed to create cover-letter event")?); 662 .context("failed to create cover-letter event")?);
661 } 663 }
662 664
@@ -883,7 +885,7 @@ pub async fn generate_patch_event(
883 .context("failed to get parent commit")?; 885 .context("failed to get parent commit")?;
884 let relay_hint = repo_ref.relays.first().map(nostr::UncheckedUrl::from); 886 let relay_hint = repo_ref.relays.first().map(nostr::UncheckedUrl::from);
885 887
886 signer.sign_event_builder(EventBuilder::new( 888 sign_event(EventBuilder::new(
887 nostr::event::Kind::Custom(PATCH_KIND), 889 nostr::event::Kind::Custom(PATCH_KIND),
888 git_repo 890 git_repo
889 .make_patch_from_commit(commit,&series_count) 891 .make_patch_from_commit(commit,&series_count)
@@ -1000,7 +1002,7 @@ pub async fn generate_patch_event(
1000 ], 1002 ],
1001 ] 1003 ]
1002 .concat(), 1004 .concat(),
1003 )).await 1005 ), signer).await
1004 .context("failed to sign event") 1006 .context("failed to sign event")
1005} 1007}
1006// TODO 1008// TODO