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:
authorDanConwayDev <DanConwayDev@protonmail.com>2025-05-23 10:01:29 +0100
committerDanConwayDev <DanConwayDev@protonmail.com>2025-05-23 10:01:29 +0100
commit686604665395385600ef8f1b5238a775249552a1 (patch)
tree56a4c7a956e14dfbcdd4a518096968abc69583a6 /src
parentcc9a3a1d8526373625246504f72f338fd89c8d8b (diff)
feat: only try http(s) for ngit-relays
otherwise it tries all the protocols and reprots on each
Diffstat (limited to 'src')
-rw-r--r--src/bin/git_remote_nostr/fetch.rs7
-rw-r--r--src/bin/git_remote_nostr/list.rs21
-rw-r--r--src/bin/git_remote_nostr/push.rs9
-rw-r--r--src/bin/git_remote_nostr/utils.rs18
-rw-r--r--src/bin/ngit/sub_commands/init.rs179
-rw-r--r--src/lib/repo_ref.rs180
6 files changed, 228 insertions, 186 deletions
diff --git a/src/bin/git_remote_nostr/fetch.rs b/src/bin/git_remote_nostr/fetch.rs
index 34a02fc..adc68b3 100644
--- a/src/bin/git_remote_nostr/fetch.rs
+++ b/src/bin/git_remote_nostr/fetch.rs
@@ -18,7 +18,7 @@ use ngit::{
18 }, 18 },
19 git_events::tag_value, 19 git_events::tag_value,
20 login::get_curent_user, 20 login::get_curent_user,
21 repo_ref::RepoRef, 21 repo_ref::{RepoRef, is_ngit_relay},
22}; 22};
23use nostr::nips::nip19; 23use nostr::nips::nip19;
24use nostr_sdk::{Event, ToBech32}; 24use nostr_sdk::{Event, ToBech32};
@@ -54,6 +54,7 @@ pub async fn run_fetch(
54 git_server_url, 54 git_server_url,
55 &repo_ref.to_nostr_git_url(&None), 55 &repo_ref.to_nostr_git_url(&None),
56 &term, 56 &term,
57 is_ngit_relay(git_server_url, &repo_ref.ngit_relays()),
57 ) { 58 ) {
58 errors.push(error); 59 errors.push(error);
59 } else { 60 } else {
@@ -163,6 +164,7 @@ pub fn fetch_from_git_server(
163 git_server_url: &str, 164 git_server_url: &str,
164 decoded_nostr_url: &NostrUrlDecoded, 165 decoded_nostr_url: &NostrUrlDecoded,
165 term: &console::Term, 166 term: &console::Term,
167 is_ngit_relay: bool,
166) -> Result<()> { 168) -> Result<()> {
167 let already_have_oids = oids 169 let already_have_oids = oids
168 .iter() 170 .iter()
@@ -173,7 +175,8 @@ pub fn fetch_from_git_server(
173 175
174 let server_url = git_server_url.parse::<CloneUrl>()?; 176 let server_url = git_server_url.parse::<CloneUrl>()?;
175 177
176 let protocols_to_attempt = get_read_protocols_to_try(git_repo, &server_url, decoded_nostr_url); 178 let protocols_to_attempt =
179 get_read_protocols_to_try(git_repo, &server_url, decoded_nostr_url, is_ngit_relay);
177 180
178 let mut failed_protocols = vec![]; 181 let mut failed_protocols = vec![];
179 let mut success = false; 182 let mut success = false;
diff --git a/src/bin/git_remote_nostr/list.rs b/src/bin/git_remote_nostr/list.rs
index 2d233ae..bf16f89 100644
--- a/src/bin/git_remote_nostr/list.rs
+++ b/src/bin/git_remote_nostr/list.rs
@@ -13,7 +13,7 @@ use ngit::{
13 }, 13 },
14 git_events::event_to_cover_letter, 14 git_events::event_to_cover_letter,
15 login::get_curent_user, 15 login::get_curent_user,
16 repo_ref, 16 repo_ref::{self, is_ngit_relay},
17}; 17};
18use nostr_sdk::hashes::sha1::Hash as Sha1Hash; 18use nostr_sdk::hashes::sha1::Hash as Sha1Hash;
19use repo_ref::RepoRef; 19use repo_ref::RepoRef;
@@ -41,6 +41,7 @@ pub async fn run_list(
41 git_repo, 41 git_repo,
42 &repo_ref.git_server, 42 &repo_ref.git_server,
43 &repo_ref.to_nostr_git_url(&None), 43 &repo_ref.to_nostr_git_url(&None),
44 &repo_ref.ngit_relays(),
44 ); 45 );
45 46
46 let mut state = if let Some(nostr_state) = nostr_state { 47 let mut state = if let Some(nostr_state) = nostr_state {
@@ -131,6 +132,7 @@ async fn get_open_and_draft_proposals_state(
131 git_server_url, 132 git_server_url,
132 &repo_ref.to_nostr_git_url(&None), 133 &repo_ref.to_nostr_git_url(&None),
133 term, 134 term,
135 is_ngit_relay(git_server_url, &repo_ref.ngit_relays()),
134 ) 136 )
135 .is_ok() 137 .is_ok()
136 { 138 {
@@ -174,12 +176,19 @@ pub fn list_from_remotes(
174 term: &console::Term, 176 term: &console::Term,
175 git_repo: &Repo, 177 git_repo: &Repo,
176 git_servers: &Vec<String>, 178 git_servers: &Vec<String>,
177 decoded_nostr_url: &NostrUrlDecoded, // Add this parameter 179 decoded_nostr_url: &NostrUrlDecoded,
180 ngit_relays: &[String],
178) -> HashMap<String, HashMap<String, String>> { 181) -> HashMap<String, HashMap<String, String>> {
179 let mut remote_states = HashMap::new(); 182 let mut remote_states = HashMap::new();
180 let mut errors = HashMap::new(); 183 let mut errors = HashMap::new();
181 for url in git_servers { 184 for url in git_servers {
182 match list_from_remote(term, git_repo, url, decoded_nostr_url) { 185 match list_from_remote(
186 term,
187 git_repo,
188 url,
189 decoded_nostr_url,
190 is_ngit_relay(url, ngit_relays),
191 ) {
183 Err(error) => { 192 Err(error) => {
184 errors.insert(url, error); 193 errors.insert(url, error);
185 } 194 }
@@ -195,10 +204,12 @@ pub fn list_from_remote(
195 term: &console::Term, 204 term: &console::Term,
196 git_repo: &Repo, 205 git_repo: &Repo,
197 git_server_url: &str, 206 git_server_url: &str,
198 decoded_nostr_url: &NostrUrlDecoded, // Add this parameter 207 decoded_nostr_url: &NostrUrlDecoded,
208 is_ngit_relay: bool,
199) -> Result<HashMap<String, String>> { 209) -> Result<HashMap<String, String>> {
200 let server_url = git_server_url.parse::<CloneUrl>()?; 210 let server_url = git_server_url.parse::<CloneUrl>()?;
201 let protocols_to_attempt = get_read_protocols_to_try(git_repo, &server_url, decoded_nostr_url); 211 let protocols_to_attempt =
212 get_read_protocols_to_try(git_repo, &server_url, decoded_nostr_url, is_ngit_relay);
202 213
203 let mut failed_protocols = vec![]; 214 let mut failed_protocols = vec![];
204 let mut remote_state: Option<HashMap<String, String>> = None; 215 let mut remote_state: Option<HashMap<String, String>> = None;
diff --git a/src/bin/git_remote_nostr/push.rs b/src/bin/git_remote_nostr/push.rs
index 4eae92f..a34d729 100644
--- a/src/bin/git_remote_nostr/push.rs
+++ b/src/bin/git_remote_nostr/push.rs
@@ -27,7 +27,7 @@ use ngit::{
27 }, 27 },
28 git_events::{self, event_to_cover_letter, get_event_root}, 28 git_events::{self, event_to_cover_letter, get_event_root},
29 login::{self, user::UserRef}, 29 login::{self, user::UserRef},
30 repo_ref::{self, get_repo_config_from_yaml}, 30 repo_ref::{self, get_repo_config_from_yaml, is_ngit_relay},
31 repo_state, 31 repo_state,
32}; 32};
33use nostr::nips::nip10::Marker; 33use nostr::nips::nip10::Marker;
@@ -49,6 +49,7 @@ use crate::{
49 }, 49 },
50}; 50};
51 51
52#[allow(clippy::too_many_lines)]
52pub async fn run_push( 53pub async fn run_push(
53 git_repo: &Repo, 54 git_repo: &Repo,
54 repo_ref: &RepoRef, 55 repo_ref: &RepoRef,
@@ -79,6 +80,7 @@ pub async fn run_push(
79 git_repo, 80 git_repo,
80 &repo_ref.git_server, 81 &repo_ref.git_server,
81 &repo_ref.to_nostr_git_url(&None), 82 &repo_ref.to_nostr_git_url(&None),
83 &repo_ref.ngit_relays(),
82 ) 84 )
83 }); 85 });
84 86
@@ -161,6 +163,7 @@ pub async fn run_push(
161 &repo_ref.to_nostr_git_url(&None), 163 &repo_ref.to_nostr_git_url(&None),
162 &remote_refspecs, 164 &remote_refspecs,
163 &term, 165 &term,
166 is_ngit_relay(&git_server_url, &repo_ref.ngit_relays()),
164 ); 167 );
165 } 168 }
166 } 169 }
@@ -412,9 +415,11 @@ fn push_to_remote(
412 decoded_nostr_url: &NostrUrlDecoded, 415 decoded_nostr_url: &NostrUrlDecoded,
413 remote_refspecs: &[String], 416 remote_refspecs: &[String],
414 term: &Term, 417 term: &Term,
418 is_ngit_relay: bool,
415) -> Result<()> { 419) -> Result<()> {
416 let server_url = git_server_url.parse::<CloneUrl>()?; 420 let server_url = git_server_url.parse::<CloneUrl>()?;
417 let protocols_to_attempt = get_write_protocols_to_try(git_repo, &server_url, decoded_nostr_url); 421 let protocols_to_attempt =
422 get_write_protocols_to_try(git_repo, &server_url, decoded_nostr_url, is_ngit_relay);
418 423
419 let mut failed_protocols = vec![]; 424 let mut failed_protocols = vec![];
420 let mut success = false; 425 let mut success = false;
diff --git a/src/bin/git_remote_nostr/utils.rs b/src/bin/git_remote_nostr/utils.rs
index 3a9f07d..2c77baa 100644
--- a/src/bin/git_remote_nostr/utils.rs
+++ b/src/bin/git_remote_nostr/utils.rs
@@ -216,8 +216,15 @@ pub fn get_read_protocols_to_try(
216 git_repo: &Repo, 216 git_repo: &Repo,
217 server_url: &CloneUrl, 217 server_url: &CloneUrl,
218 decoded_nostr_url: &NostrUrlDecoded, 218 decoded_nostr_url: &NostrUrlDecoded,
219 is_ngit_relay: bool,
219) -> Vec<ServerProtocol> { 220) -> Vec<ServerProtocol> {
220 if server_url.protocol() == ServerProtocol::Filesystem { 221 if is_ngit_relay {
222 if server_url.protocol() == ServerProtocol::Http {
223 vec![(ServerProtocol::UnauthHttp)]
224 } else {
225 vec![(ServerProtocol::UnauthHttps)]
226 }
227 } else if server_url.protocol() == ServerProtocol::Filesystem {
221 vec![(ServerProtocol::Filesystem)] 228 vec![(ServerProtocol::Filesystem)]
222 } else if let Some(protocol) = &decoded_nostr_url.protocol { 229 } else if let Some(protocol) = &decoded_nostr_url.protocol {
223 vec![protocol.clone()] 230 vec![protocol.clone()]
@@ -254,8 +261,15 @@ pub fn get_write_protocols_to_try(
254 git_repo: &Repo, 261 git_repo: &Repo,
255 server_url: &CloneUrl, 262 server_url: &CloneUrl,
256 decoded_nostr_url: &NostrUrlDecoded, 263 decoded_nostr_url: &NostrUrlDecoded,
264 is_ngit_relay: bool,
257) -> Vec<ServerProtocol> { 265) -> Vec<ServerProtocol> {
258 if server_url.protocol() == ServerProtocol::Filesystem { 266 if is_ngit_relay {
267 if server_url.protocol() == ServerProtocol::Http {
268 vec![(ServerProtocol::UnauthHttp)]
269 } else {
270 vec![(ServerProtocol::UnauthHttps)]
271 }
272 } else if server_url.protocol() == ServerProtocol::Filesystem {
259 vec![(ServerProtocol::Filesystem)] 273 vec![(ServerProtocol::Filesystem)]
260 } else if let Some(protocol) = &decoded_nostr_url.protocol { 274 } else if let Some(protocol) = &decoded_nostr_url.protocol {
261 vec![protocol.clone()] 275 vec![protocol.clone()]
diff --git a/src/bin/ngit/sub_commands/init.rs b/src/bin/ngit/sub_commands/init.rs
index 41ed407..4b0f7b1 100644
--- a/src/bin/ngit/sub_commands/init.rs
+++ b/src/bin/ngit/sub_commands/init.rs
@@ -14,7 +14,10 @@ use ngit::{
14 cli_interactor::{PromptChoiceParms, PromptConfirmParms, PromptMultiChoiceParms}, 14 cli_interactor::{PromptChoiceParms, PromptConfirmParms, PromptMultiChoiceParms},
15 client::{Params, send_events}, 15 client::{Params, send_events},
16 git::nostr_url::{CloneUrl, NostrUrlDecoded}, 16 git::nostr_url::{CloneUrl, NostrUrlDecoded},
17 repo_ref::{extract_pks, save_repo_config_to_yaml}, 17 repo_ref::{
18 detect_existing_ngit_relays, extract_npub, extract_pks, normalize_ngit_relay_url,
19 save_repo_config_to_yaml,
20 },
18}; 21};
19use nostr::{ 22use nostr::{
20 FromBech32, PublicKey, ToBech32, 23 FromBech32, PublicKey, ToBech32,
@@ -869,117 +872,6 @@ where
869 Ok(selected_choices) 872 Ok(selected_choices)
870} 873}
871 874
872fn detect_existing_ngit_relays(
873 repo_ref: Option<&RepoRef>,
874 args_relays: &[String],
875 args_clone_url: &[String],
876 args_blossoms: &[String],
877 identifier: &str,
878) -> Vec<String> {
879 // Collect clone URLs from arguments or repo_ref
880 let clone_urls: Vec<String> = if !args_clone_url.is_empty() {
881 args_clone_url.to_vec()
882 } else if let Some(repo) = repo_ref {
883 repo.git_server.clone()
884 } else {
885 Vec::new()
886 };
887
888 // Collect relays from arguments or repo_ref
889 let relays: Vec<RelayUrl> = if !args_relays.is_empty() {
890 args_relays
891 .iter()
892 .filter_map(|r| RelayUrl::parse(r).ok())
893 .collect()
894 } else if let Some(repo) = repo_ref {
895 repo.relays.clone()
896 } else {
897 Vec::new()
898 };
899
900 // Collect blossom server URLs from arguments or repo_ref
901 let blossoms: Vec<Url> = if !args_blossoms.is_empty() {
902 args_blossoms
903 .iter()
904 .filter_map(|r| Url::parse(r).ok())
905 .collect()
906 } else if let Some(repo) = repo_ref {
907 repo.blossoms.clone()
908 } else {
909 Vec::new()
910 };
911
912 let mut existing_ngit_relays = Vec::new();
913 for url in &clone_urls {
914 let Ok(formatted_as_ngit_relay_url) = normalize_ngit_relay_url(url) else {
915 continue;
916 };
917 if existing_ngit_relays.contains(&formatted_as_ngit_relay_url) {
918 continue;
919 }
920
921 let clone_url_is_ngit_relay_format = if let Ok(npub) = extract_npub(url) {
922 url.contains(&format!("/{npub}/{identifier}.git"))
923 } else {
924 false
925 };
926 if !clone_url_is_ngit_relay_format {
927 continue;
928 }
929
930 let matches_relay = relays.iter().any(|r| {
931 normalize_ngit_relay_url(&r.to_string())
932 .is_ok_and(|r| r.eq(&formatted_as_ngit_relay_url))
933 });
934 if !matches_relay {
935 continue;
936 }
937
938 let matches_blossoms = blossoms.iter().any(|r| {
939 normalize_ngit_relay_url(r.as_str()).is_ok_and(|r| r.eq(&formatted_as_ngit_relay_url))
940 });
941 if !matches_blossoms {
942 continue;
943 }
944
945 existing_ngit_relays.push(formatted_as_ngit_relay_url);
946 }
947 existing_ngit_relays
948}
949
950fn normalize_ngit_relay_url(url: &str) -> Result<String> {
951 // Parse the URL and handle errors
952 let mut parsed = Url::parse(url)
953 .or_else(|_| Url::parse(&format!("https://{url}")))
954 .context(format!("{url} not a valid ngit relay URL"))?;
955 if parsed.host_str().is_none() {
956 // so sub.domain.org gets identifier as host in "sub.domain.org"
957 parsed = Url::parse(&format!("https://{url}"))?;
958 }
959
960 // Extract the scheme, host, port, and path
961 let scheme = parsed.scheme();
962 let host = parsed.host_str().context(format!(
963 "{url} not a ngit relay url reference: missing host in URL {parsed}"
964 ))?;
965 let port = parsed.port().map(|p| format!(":{p}")).unwrap_or_default();
966 let path = parsed.path();
967
968 // Normalize the URL based on the scheme and path
969 let mut normalized_url = match scheme {
970 "ws" | "http" => format!("http://{host}{port}{path}"),
971 _ => format!("{host}{port}{path}"),
972 };
973
974 // If the normalized URL contains "npub1", remove "npub1" and everything after
975 // it
976 if let Some(pos) = normalized_url.find("npub1") {
977 normalized_url.truncate(pos); // Keep everything before "npub1"
978 }
979 // Return the normalized URL
980 Ok(normalized_url.trim_end_matches('/').to_string())
981}
982
983fn format_ngit_relay_url_as_clone_url( 875fn format_ngit_relay_url_as_clone_url(
984 url: &str, 876 url: &str,
985 public_key: &PublicKey, 877 public_key: &PublicKey,
@@ -1014,25 +906,6 @@ fn format_ngit_relay_url_as_blossom_url(url: &str) -> Result<String> {
1014 Ok(format!("https://{ngit_relay_url}")) 906 Ok(format!("https://{ngit_relay_url}"))
1015} 907}
1016 908
1017fn extract_npub(s: &str) -> Result<&str> {
1018 // Find the starting index of "npub1"
1019 if let Some(start) = s.find("npub1") {
1020 let mut end = start + 5; // Start after "npub1"
1021
1022 // Move the end index to include valid characters (0-9, a-z)
1023 while end < s.len() && s[end..=end].chars().all(|c| c.is_ascii_alphanumeric()) {
1024 end += 1;
1025 }
1026 // Extract the npub substring
1027 let npub = &s[start..end];
1028 // Attempt to create a PublicKey from the extracted npub
1029 PublicKey::from_bech32(npub).context("invalid npub")?;
1030 Ok(npub)
1031 } else {
1032 bail!("No npub found")
1033 }
1034}
1035
1036fn parse_relay_url(s: &str) -> Result<RelayUrl> { 909fn parse_relay_url(s: &str) -> Result<RelayUrl> {
1037 // Attempt to parse the original string 910 // Attempt to parse the original string
1038 match RelayUrl::parse(s) { 911 match RelayUrl::parse(s) {
@@ -1098,47 +971,3 @@ fn push_main_or_master_branch(git_repo: &Repo) -> Result<()> {
1098 bail!("git push process exited with an error: {}", exit_status); 971 bail!("git push process exited with an error: {}", exit_status);
1099 } 972 }
1100} 973}
1101
1102#[cfg(test)]
1103mod tests {
1104 use anyhow::Result;
1105
1106 use super::*;
1107
1108 #[test]
1109 fn normalize_ngit_relay_url_all_checks() -> Result<()> {
1110 let test_cases = vec![
1111 ("https://sub.domain.org", "sub.domain.org"),
1112 ("wss://sub.domain.org", "sub.domain.org"),
1113 ("sub.domain.org", "sub.domain.org"),
1114 ("http://sub.domain.org", "http://sub.domain.org"),
1115 ("ws://sub.domain.org", "http://sub.domain.org"),
1116 ("http://localhost", "http://localhost"),
1117 ("localhost", "localhost"),
1118 ("https://sub.domain.org:8080", "sub.domain.org:8080"),
1119 ("http://sub.domain.org:8080", "http://sub.domain.org:8080"),
1120 ("sub.domain.org:8080", "sub.domain.org:8080"),
1121 ("https://sub.domain.org/path/to", "sub.domain.org/path/to"),
1122 (
1123 "https://sub.domain.org:8080/path/to",
1124 "sub.domain.org:8080/path/to",
1125 ),
1126 (
1127 "https://sub.domain.org/npub143675782648/to.git",
1128 "sub.domain.org",
1129 ),
1130 (
1131 "https://sub.domain.org/path/npub143675782648/to.git",
1132 "sub.domain.org/path",
1133 ),
1134 ("https://sub.domain.org/", "sub.domain.org"),
1135 ("http://sub.domain.org/", "http://sub.domain.org"),
1136 ];
1137
1138 for (input, expected) in test_cases {
1139 let normalized = normalize_ngit_relay_url(input)?;
1140 assert_eq!(normalized, expected);
1141 }
1142 Ok(())
1143 }
1144}
diff --git a/src/lib/repo_ref.rs b/src/lib/repo_ref.rs
index df1427a..aa36bbb 100644
--- a/src/lib/repo_ref.rs
+++ b/src/lib/repo_ref.rs
@@ -290,6 +290,10 @@ impl RepoRef {
290 user: None, 290 user: None,
291 } 291 }
292 } 292 }
293
294 pub fn ngit_relays(&self) -> Vec<String> {
295 detect_existing_ngit_relays(Some(self), &[], &[], &[], &self.identifier)
296 }
293} 297}
294 298
295pub async fn get_repo_coordinates_when_remote_unknown( 299pub async fn get_repo_coordinates_when_remote_unknown(
@@ -565,6 +569,145 @@ pub fn save_repo_config_to_yaml(
565 .context("failed to write maintainers to maintainers.yaml file serde_yaml") 569 .context("failed to write maintainers to maintainers.yaml file serde_yaml")
566} 570}
567 571
572pub fn detect_existing_ngit_relays(
573 repo_ref: Option<&RepoRef>,
574 args_relays: &[String],
575 args_clone_url: &[String],
576 args_blossoms: &[String],
577 identifier: &str,
578) -> Vec<String> {
579 // Collect clone URLs from arguments or repo_ref
580 let clone_urls: Vec<String> = if !args_clone_url.is_empty() {
581 args_clone_url.to_vec()
582 } else if let Some(repo) = repo_ref {
583 repo.git_server.clone()
584 } else {
585 Vec::new()
586 };
587
588 // Collect relays from arguments or repo_ref
589 let relays: Vec<RelayUrl> = if !args_relays.is_empty() {
590 args_relays
591 .iter()
592 .filter_map(|r| RelayUrl::parse(r).ok())
593 .collect()
594 } else if let Some(repo) = repo_ref {
595 repo.relays.clone()
596 } else {
597 Vec::new()
598 };
599
600 // Collect blossom server URLs from arguments or repo_ref
601 let blossoms: Vec<Url> = if !args_blossoms.is_empty() {
602 args_blossoms
603 .iter()
604 .filter_map(|r| Url::parse(r).ok())
605 .collect()
606 } else if let Some(repo) = repo_ref {
607 repo.blossoms.clone()
608 } else {
609 Vec::new()
610 };
611
612 let mut existing_ngit_relays = Vec::new();
613 for url in &clone_urls {
614 let Ok(formatted_as_ngit_relay_url) = normalize_ngit_relay_url(url) else {
615 continue;
616 };
617 if existing_ngit_relays.contains(&formatted_as_ngit_relay_url) {
618 continue;
619 }
620
621 let clone_url_is_ngit_relay_format = if let Ok(npub) = extract_npub(url) {
622 url.contains(&format!("/{npub}/{identifier}.git"))
623 } else {
624 false
625 };
626 if !clone_url_is_ngit_relay_format {
627 continue;
628 }
629
630 let matches_relay = relays.iter().any(|r| {
631 normalize_ngit_relay_url(&r.to_string())
632 .is_ok_and(|r| r.eq(&formatted_as_ngit_relay_url))
633 });
634 if !matches_relay {
635 continue;
636 }
637
638 let matches_blossoms = blossoms.iter().any(|r| {
639 normalize_ngit_relay_url(r.as_str()).is_ok_and(|r| r.eq(&formatted_as_ngit_relay_url))
640 });
641 if !matches_blossoms {
642 continue;
643 }
644
645 existing_ngit_relays.push(formatted_as_ngit_relay_url);
646 }
647 existing_ngit_relays
648}
649
650pub fn normalize_ngit_relay_url(url: &str) -> Result<String> {
651 // Parse the URL and handle errors
652 let mut parsed = Url::parse(url)
653 .or_else(|_| Url::parse(&format!("https://{url}")))
654 .context(format!("{url} not a valid ngit relay URL"))?;
655 if parsed.host_str().is_none() {
656 // so sub.domain.org gets identifier as host in "sub.domain.org"
657 parsed = Url::parse(&format!("https://{url}"))?;
658 }
659
660 // Extract the scheme, host, port, and path
661 let scheme = parsed.scheme();
662 let host = parsed.host_str().context(format!(
663 "{url} not a ngit relay url reference: missing host in URL {parsed}"
664 ))?;
665 let port = parsed.port().map(|p| format!(":{p}")).unwrap_or_default();
666 let path = parsed.path();
667
668 // Normalize the URL based on the scheme and path
669 let mut normalized_url = match scheme {
670 "ws" | "http" => format!("http://{host}{port}{path}"),
671 _ => format!("{host}{port}{path}"),
672 };
673
674 // If the normalized URL contains "npub1", remove "npub1" and everything after
675 // it
676 if let Some(pos) = normalized_url.find("npub1") {
677 normalized_url.truncate(pos); // Keep everything before "npub1"
678 }
679 // Return the normalized URL
680 Ok(normalized_url.trim_end_matches('/').to_string())
681}
682
683pub fn extract_npub(s: &str) -> Result<&str> {
684 // Find the starting index of "npub1"
685 if let Some(start) = s.find("npub1") {
686 let mut end = start + 5; // Start after "npub1"
687
688 // Move the end index to include valid characters (0-9, a-z)
689 while end < s.len() && s[end..=end].chars().all(|c| c.is_ascii_alphanumeric()) {
690 end += 1;
691 }
692 // Extract the npub substring
693 let npub = &s[start..end];
694 // Attempt to create a PublicKey from the extracted npub
695 PublicKey::from_bech32(npub).context("invalid npub")?;
696 Ok(npub)
697 } else {
698 bail!("No npub found")
699 }
700}
701
702pub fn is_ngit_relay(url: &str, ngit_relays: &[String]) -> bool {
703 if !ngit_relays.is_empty() {
704 if let Ok(n) = normalize_ngit_relay_url(url) {
705 return ngit_relays.contains(&n);
706 }
707 }
708 false
709}
710
568#[cfg(test)] 711#[cfg(test)]
569mod tests { 712mod tests {
570 use test_utils::*; 713 use test_utils::*;
@@ -842,4 +985,41 @@ mod tests {
842 } 985 }
843 } 986 }
844 } 987 }
988
989 #[test]
990 fn normalize_ngit_relay_url_all_checks() -> Result<()> {
991 let test_cases = vec![
992 ("https://sub.domain.org", "sub.domain.org"),
993 ("wss://sub.domain.org", "sub.domain.org"),
994 ("sub.domain.org", "sub.domain.org"),
995 ("http://sub.domain.org", "http://sub.domain.org"),
996 ("ws://sub.domain.org", "http://sub.domain.org"),
997 ("http://localhost", "http://localhost"),
998 ("localhost", "localhost"),
999 ("https://sub.domain.org:8080", "sub.domain.org:8080"),
1000 ("http://sub.domain.org:8080", "http://sub.domain.org:8080"),
1001 ("sub.domain.org:8080", "sub.domain.org:8080"),
1002 ("https://sub.domain.org/path/to", "sub.domain.org/path/to"),
1003 (
1004 "https://sub.domain.org:8080/path/to",
1005 "sub.domain.org:8080/path/to",
1006 ),
1007 (
1008 "https://sub.domain.org/npub143675782648/to.git",
1009 "sub.domain.org",
1010 ),
1011 (
1012 "https://sub.domain.org/path/npub143675782648/to.git",
1013 "sub.domain.org/path",
1014 ),
1015 ("https://sub.domain.org/", "sub.domain.org"),
1016 ("http://sub.domain.org/", "http://sub.domain.org"),
1017 ];
1018
1019 for (input, expected) in test_cases {
1020 let normalized = normalize_ngit_relay_url(input)?;
1021 assert_eq!(normalized, expected);
1022 }
1023 Ok(())
1024 }
845} 1025}