diff options
| -rw-r--r-- | src/bin/ngit/sub_commands/init.rs | 106 | ||||
| -rw-r--r-- | src/bin/ngit/sub_commands/send.rs | 275 | ||||
| -rw-r--r-- | src/lib/cli_interactor.rs | 87 | ||||
| -rw-r--r-- | src/lib/client.rs | 54 | ||||
| -rw-r--r-- | src/lib/git_events.rs | 1 | ||||
| -rw-r--r-- | src/lib/login/user.rs | 77 | ||||
| -rw-r--r-- | src/lib/repo_ref.rs | 83 |
7 files changed, 519 insertions, 164 deletions
diff --git a/src/bin/ngit/sub_commands/init.rs b/src/bin/ngit/sub_commands/init.rs index eaaf83d..01fcaea 100644 --- a/src/bin/ngit/sub_commands/init.rs +++ b/src/bin/ngit/sub_commands/init.rs | |||
| @@ -12,12 +12,12 @@ use console::{Style, Term}; | |||
| 12 | use dialoguer::theme::{ColorfulTheme, Theme}; | 12 | use dialoguer::theme::{ColorfulTheme, Theme}; |
| 13 | use ngit::{ | 13 | use ngit::{ |
| 14 | UrlWithoutSlash, | 14 | UrlWithoutSlash, |
| 15 | cli_interactor::{PromptChoiceParms, PromptConfirmParms, PromptMultiChoiceParms}, | 15 | cli_interactor::{PromptChoiceParms, PromptConfirmParms, multi_select_with_custom_value}, |
| 16 | client::{Params, send_events}, | 16 | client::{Params, send_events}, |
| 17 | git::nostr_url::{CloneUrl, NostrUrlDecoded}, | 17 | git::nostr_url::{CloneUrl, NostrUrlDecoded}, |
| 18 | repo_ref::{ | 18 | repo_ref::{ |
| 19 | detect_existing_grasp_servers, extract_npub, extract_pks, normalize_grasp_server_url, | 19 | detect_existing_grasp_servers, extract_npub, extract_pks, |
| 20 | save_repo_config_to_yaml, | 20 | format_grasp_server_url_as_relay_url, normalize_grasp_server_url, save_repo_config_to_yaml, |
| 21 | }, | 21 | }, |
| 22 | }; | 22 | }; |
| 23 | use nostr::{ | 23 | use nostr::{ |
| @@ -727,6 +727,11 @@ pub async fn launch(cli_args: &Cli, args: &SubCommandArgs) -> Result<()> { | |||
| 727 | web, | 727 | web, |
| 728 | relays: relays.clone(), | 728 | relays: relays.clone(), |
| 729 | blossoms, | 729 | blossoms, |
| 730 | hashtags: if let Some(repo_ref) = repo_ref { | ||
| 731 | repo_ref.hashtags | ||
| 732 | } else { | ||
| 733 | vec![] | ||
| 734 | }, | ||
| 730 | trusted_maintainer: user_ref.public_key, | 735 | trusted_maintainer: user_ref.public_key, |
| 731 | maintainers_without_annoucnement: None, | 736 | maintainers_without_annoucnement: None, |
| 732 | maintainers: maintainers.clone(), | 737 | maintainers: maintainers.clone(), |
| @@ -848,93 +853,6 @@ pub async fn launch(cli_args: &Cli, args: &SubCommandArgs) -> Result<()> { | |||
| 848 | Ok(()) | 853 | Ok(()) |
| 849 | } | 854 | } |
| 850 | 855 | ||
| 851 | fn multi_select_with_custom_value<F>( | ||
| 852 | prompt: &str, | ||
| 853 | custom_choice_prompt: &str, | ||
| 854 | mut choices: Vec<String>, | ||
| 855 | mut defaults: Vec<bool>, | ||
| 856 | validate_choice: F, | ||
| 857 | ) -> Result<Vec<String>> | ||
| 858 | where | ||
| 859 | F: Fn(&str) -> Result<String>, | ||
| 860 | { | ||
| 861 | let mut selected_choices = vec![]; | ||
| 862 | |||
| 863 | // Loop to allow users to add more choices | ||
| 864 | loop { | ||
| 865 | // Add 'add another' option at the end of the choices | ||
| 866 | let mut current_choices = choices.clone(); | ||
| 867 | current_choices.push(if current_choices.is_empty() { | ||
| 868 | "add".to_string() | ||
| 869 | } else { | ||
| 870 | "add another".to_string() | ||
| 871 | }); | ||
| 872 | |||
| 873 | // Create default selections based on the provided defaults | ||
| 874 | let mut current_defaults = defaults.clone(); | ||
| 875 | current_defaults.push(current_choices.len() == 1); // 'add another' should not be selected by default | ||
| 876 | |||
| 877 | // Prompt for selections | ||
| 878 | let selected_indices: Vec<usize> = Interactor::default().multi_choice( | ||
| 879 | PromptMultiChoiceParms::default() | ||
| 880 | .with_prompt(prompt) | ||
| 881 | .dont_report() | ||
| 882 | .with_choices(current_choices.clone()) | ||
| 883 | .with_defaults(current_defaults), | ||
| 884 | )?; | ||
| 885 | |||
| 886 | // Collect selected choices | ||
| 887 | selected_choices.clear(); // Clear previous selections to update | ||
| 888 | for &index in &selected_indices { | ||
| 889 | if index < choices.len() { | ||
| 890 | // Exclude 'add another' option | ||
| 891 | selected_choices.push(choices[index].clone()); | ||
| 892 | } | ||
| 893 | } | ||
| 894 | |||
| 895 | // Check if 'add another' was selected | ||
| 896 | if selected_indices.contains(&(choices.len())) { | ||
| 897 | // Last index is 'add another' | ||
| 898 | let mut new_choice: String; | ||
| 899 | loop { | ||
| 900 | new_choice = Interactor::default().input( | ||
| 901 | PromptInputParms::default() | ||
| 902 | .with_prompt(custom_choice_prompt) | ||
| 903 | .dont_report() | ||
| 904 | .optional(), | ||
| 905 | )?; | ||
| 906 | |||
| 907 | if new_choice.is_empty() { | ||
| 908 | break; | ||
| 909 | } | ||
| 910 | // Validate the new choice | ||
| 911 | match validate_choice(&new_choice) { | ||
| 912 | Ok(valid_choice) => { | ||
| 913 | new_choice = valid_choice; // Use the fixed version of the input | ||
| 914 | break; // Valid choice, exit the loop | ||
| 915 | } | ||
| 916 | Err(err) => { | ||
| 917 | // Inform the user about the validation error | ||
| 918 | println!("Error: {err}"); | ||
| 919 | } | ||
| 920 | } | ||
| 921 | } | ||
| 922 | |||
| 923 | // Add the new choice to the choices vector | ||
| 924 | if !new_choice.is_empty() { | ||
| 925 | choices.push(new_choice.clone()); // Add new choice to the end of the list | ||
| 926 | selected_choices.push(new_choice); // Automatically select the new choice | ||
| 927 | defaults.push(true); // Set the new choice as selected by default | ||
| 928 | } | ||
| 929 | } else { | ||
| 930 | // Exit the loop if 'add another' was not selected | ||
| 931 | break; | ||
| 932 | } | ||
| 933 | } | ||
| 934 | |||
| 935 | Ok(selected_choices) | ||
| 936 | } | ||
| 937 | |||
| 938 | fn format_grasp_server_url_as_clone_url( | 856 | fn format_grasp_server_url_as_clone_url( |
| 939 | url: &str, | 857 | url: &str, |
| 940 | public_key: &PublicKey, | 858 | public_key: &PublicKey, |
| @@ -953,14 +871,6 @@ fn format_grasp_server_url_as_clone_url( | |||
| 953 | )) | 871 | )) |
| 954 | } | 872 | } |
| 955 | 873 | ||
| 956 | fn format_grasp_server_url_as_relay_url(url: &str) -> Result<String> { | ||
| 957 | let grasp_server_url = normalize_grasp_server_url(url)?; | ||
| 958 | if grasp_server_url.contains("http://") { | ||
| 959 | return Ok(grasp_server_url.replace("http://", "ws://")); | ||
| 960 | } | ||
| 961 | Ok(format!("wss://{grasp_server_url}")) | ||
| 962 | } | ||
| 963 | |||
| 964 | fn format_grasp_server_url_as_blossom_url(url: &str) -> Result<String> { | 874 | fn format_grasp_server_url_as_blossom_url(url: &str) -> Result<String> { |
| 965 | let grasp_server_url = normalize_grasp_server_url(url)?; | 875 | let grasp_server_url = normalize_grasp_server_url(url)?; |
| 966 | if grasp_server_url.contains("http://") { | 876 | if grasp_server_url.contains("http://") { |
diff --git a/src/bin/ngit/sub_commands/send.rs b/src/bin/ngit/sub_commands/send.rs index 609812b..835153e 100644 --- a/src/bin/ngit/sub_commands/send.rs +++ b/src/bin/ngit/sub_commands/send.rs | |||
| @@ -1,16 +1,27 @@ | |||
| 1 | use std::{path::Path, str::FromStr}; | 1 | use std::{path::Path, str::FromStr, thread, time::Duration}; |
| 2 | 2 | ||
| 3 | use anyhow::{Context, Result, bail}; | 3 | use anyhow::{Context, Result, bail}; |
| 4 | use console::Style; | 4 | use console::Style; |
| 5 | use ngit::{ | 5 | use ngit::{ |
| 6 | cli_interactor::{PromptChoiceParms, multi_select_with_custom_value}, | ||
| 6 | client::{Params, send_events}, | 7 | client::{Params, send_events}, |
| 7 | git::nostr_url::CloneUrl, | 8 | git::nostr_url::CloneUrl, |
| 8 | git_events::{EventRefType, KIND_PULL_REQUEST, generate_cover_letter_and_patch_events}, | 9 | git_events::{EventRefType, KIND_PULL_REQUEST, generate_cover_letter_and_patch_events}, |
| 9 | push::push_refs_and_generate_pr_or_pr_update_event, | 10 | push::push_refs_and_generate_pr_or_pr_update_event, |
| 10 | repo_ref::is_grasp_server, | 11 | repo_ref::{ |
| 12 | format_grasp_server_url_as_clone_url, format_grasp_server_url_as_relay_url, | ||
| 13 | is_grasp_server, normalize_grasp_server_url, | ||
| 14 | }, | ||
| 11 | utils::proposal_tip_is_pr_or_pr_update, | 15 | utils::proposal_tip_is_pr_or_pr_update, |
| 12 | }; | 16 | }; |
| 13 | use nostr::{ToBech32, event::Event, nips::nip19::Nip19Event}; | 17 | use nostr::{ |
| 18 | ToBech32, | ||
| 19 | event::Event, | ||
| 20 | nips::{ | ||
| 21 | nip01::Coordinate, | ||
| 22 | nip19::{Nip19Coordinate, Nip19Event}, | ||
| 23 | }, | ||
| 24 | }; | ||
| 14 | use nostr_sdk::hashes::sha1::Hash as Sha1Hash; | 25 | use nostr_sdk::hashes::sha1::Hash as Sha1Hash; |
| 15 | 26 | ||
| 16 | use crate::{ | 27 | use crate::{ |
| @@ -179,7 +190,7 @@ pub async fn launch(cli_args: &Cli, args: &SubCommandArgs, no_fetch: bool) -> Re | |||
| 179 | None | 190 | None |
| 180 | }; | 191 | }; |
| 181 | 192 | ||
| 182 | let (signer, user_ref, _) = login::login_or_signup( | 193 | let (signer, mut user_ref, _) = login::login_or_signup( |
| 183 | &Some(&git_repo), | 194 | &Some(&git_repo), |
| 184 | &extract_signer_cli_arguments(cli_args).unwrap_or(None), | 195 | &extract_signer_cli_arguments(cli_args).unwrap_or(None), |
| 185 | &cli_args.password, | 196 | &cli_args.password, |
| @@ -194,20 +205,55 @@ pub async fn launch(cli_args: &Cli, args: &SubCommandArgs, no_fetch: bool) -> Re | |||
| 194 | commits.reverse(); | 205 | commits.reverse(); |
| 195 | 206 | ||
| 196 | let events = if as_pr { | 207 | let events = if as_pr { |
| 208 | let mut to_try = vec![]; | ||
| 209 | let mut tried = vec![]; | ||
| 197 | let repo_grasps = repo_ref.grasp_servers(); | 210 | let repo_grasps = repo_ref.grasp_servers(); |
| 198 | let repo_grasp_clone_urls: Vec<String> = repo_ref | 211 | // if the user already has a fork, or is a maintainer, use those git servers |
| 199 | .git_server | 212 | let mut user_repo_ref = get_repo_ref_from_cache( |
| 200 | .iter() | 213 | Some(git_repo_path), |
| 201 | .filter(|s| is_grasp_server(s, &repo_grasps)) | 214 | &Nip19Coordinate { |
| 202 | .cloned() | 215 | coordinate: Coordinate { |
| 203 | .collect(); | 216 | kind: nostr::event::Kind::GitRepoAnnouncement, |
| 204 | if repo_grasp_clone_urls.is_empty() { | 217 | public_key: user_ref.public_key, |
| 218 | identifier: repo_ref.identifier.clone(), | ||
| 219 | }, | ||
| 220 | relays: vec![], | ||
| 221 | }, | ||
| 222 | ) | ||
| 223 | .await | ||
| 224 | .ok(); | ||
| 225 | if let Some(user_repo_ref) = &user_repo_ref { | ||
| 226 | for url in &user_repo_ref.git_server { | ||
| 227 | if CloneUrl::from_str(url).is_ok() { | ||
| 228 | to_try.push(url.clone()); | ||
| 229 | } | ||
| 230 | } | ||
| 231 | } | ||
| 232 | if !to_try.is_empty() || !repo_grasps.is_empty() { | ||
| 233 | println!( | ||
| 234 | "pushing proposal refs to {}", | ||
| 235 | if repo_ref.maintainers.contains(&user_ref.public_key) { | ||
| 236 | "repository git servers" | ||
| 237 | } else if to_try.is_empty() { | ||
| 238 | "repository grasp servers" | ||
| 239 | } else if repo_grasps.is_empty() { | ||
| 240 | "the git servers listed in your fork" | ||
| 241 | } else { | ||
| 242 | "the git servers listed in your fork and repository grasp servers" | ||
| 243 | } | ||
| 244 | ); | ||
| 245 | } else { | ||
| 205 | println!( | 246 | println!( |
| 206 | "The repository doesn't list a grasp server which would otherwise be used to submit your proposal as nostr Pull Request." | 247 | "The repository doesn't list a grasp server which would otherwise be used to submit your proposal as nostr Pull Request." |
| 207 | ); | 248 | ); |
| 208 | } | 249 | } |
| 209 | let mut to_try = repo_grasp_clone_urls.clone(); | 250 | // also use repo grasp servers |
| 210 | let mut tried = vec![]; | 251 | for url in &repo_ref.git_server { |
| 252 | if is_grasp_server(url, &repo_grasps) && !to_try.contains(url) { | ||
| 253 | to_try.push(url.clone()); | ||
| 254 | } | ||
| 255 | } | ||
| 256 | |||
| 211 | let mut git_ref = None; | 257 | let mut git_ref = None; |
| 212 | loop { | 258 | loop { |
| 213 | let (events, _server_responses) = push_refs_and_generate_pr_or_pr_update_event( | 259 | let (events, _server_responses) = push_refs_and_generate_pr_or_pr_update_event( |
| @@ -217,7 +263,7 @@ pub async fn launch(cli_args: &Cli, args: &SubCommandArgs, no_fetch: bool) -> Re | |||
| 217 | &user_ref, | 263 | &user_ref, |
| 218 | root_proposal.as_ref(), | 264 | root_proposal.as_ref(), |
| 219 | &cover_letter_title_description, | 265 | &cover_letter_title_description, |
| 220 | &repo_grasp_clone_urls, | 266 | &to_try, |
| 221 | git_ref.clone(), | 267 | git_ref.clone(), |
| 222 | &signer, | 268 | &signer, |
| 223 | &console::Term::stdout(), | 269 | &console::Term::stdout(), |
| @@ -230,27 +276,194 @@ pub async fn launch(cli_args: &Cli, args: &SubCommandArgs, no_fetch: bool) -> Re | |||
| 230 | if let Some(events) = events { | 276 | if let Some(events) = events { |
| 231 | break events; | 277 | break events; |
| 232 | } | 278 | } |
| 233 | let clone_url = Interactor::default() | 279 | // fallback to creating user personal-fork on their grasp servers |
| 234 | .input( | 280 | let untried_user_grasp_servers: Vec<String> = user_ref |
| 235 | PromptInputParms::default().with_prompt("git repo url with write permission"), | 281 | .grasp_list |
| 236 | )? | 282 | .urls |
| 237 | .clone(); | 283 | .iter() |
| 238 | if CloneUrl::from_str(&clone_url).is_ok() { | 284 | .map(std::string::ToString::to_string) |
| 239 | to_try.push(clone_url); | 285 | .filter(|g| { |
| 240 | let mut git_ref_or_branch_name = Interactor::default() | 286 | // is a grasp server not in list of tried |
| 241 | .input( | 287 | !is_grasp_server(g, &tried) |
| 242 | PromptInputParms::default() | 288 | }) |
| 243 | .with_prompt("ref / branch name") | 289 | .collect(); |
| 244 | .with_default(git_ref.unwrap_or("refs/nostr/<event-id>".to_string())), | 290 | |
| 291 | if untried_user_grasp_servers.is_empty() | ||
| 292 | && Interactor::default().choice( | ||
| 293 | PromptChoiceParms::default() | ||
| 294 | .with_prompt("choose alternative git server") | ||
| 295 | .dont_report() | ||
| 296 | .with_choices(vec![ | ||
| 297 | "choose grasp server(s)".to_string(), | ||
| 298 | "enter a git repo url with write permission".to_string(), | ||
| 299 | ]) | ||
| 300 | .with_default(0), | ||
| 301 | )? == 1 | ||
| 302 | { | ||
| 303 | loop { | ||
| 304 | let clone_url = Interactor::default() | ||
| 305 | .input( | ||
| 306 | PromptInputParms::default() | ||
| 307 | .with_prompt("git repo url with write permission"), | ||
| 308 | )? | ||
| 309 | .clone(); | ||
| 310 | if CloneUrl::from_str(&clone_url).is_ok() { | ||
| 311 | to_try.push(clone_url); | ||
| 312 | let mut git_ref_or_branch_name = Interactor::default() | ||
| 313 | .input( | ||
| 314 | PromptInputParms::default() | ||
| 315 | .with_prompt("ref / branch name") | ||
| 316 | .with_default( | ||
| 317 | git_ref.unwrap_or("refs/nostr/<event-id>".to_string()), | ||
| 318 | ), | ||
| 319 | )? | ||
| 320 | .clone(); | ||
| 321 | if !git_ref_or_branch_name.starts_with("refs/") { | ||
| 322 | git_ref_or_branch_name = format!("refs/heads/{git_ref_or_branch_name}"); | ||
| 323 | } | ||
| 324 | git_ref = Some(git_ref_or_branch_name); | ||
| 325 | break; | ||
| 326 | } | ||
| 327 | println!("invalid clone url"); | ||
| 328 | } | ||
| 329 | continue; | ||
| 330 | } | ||
| 331 | |||
| 332 | let mut new_grasp_server_events: Vec<Event> = vec![]; | ||
| 333 | |||
| 334 | let grasp_servers = if untried_user_grasp_servers.is_empty() { | ||
| 335 | let default_choices: Vec<String> = client | ||
| 336 | .get_grasp_default_set() | ||
| 337 | .iter() | ||
| 338 | .filter(|g| !is_grasp_server(g, &tried)) | ||
| 339 | .cloned() | ||
| 340 | .collect(); | ||
| 341 | let selections = vec![true; default_choices.len()]; // all selected by default | ||
| 342 | let grasp_servers = multi_select_with_custom_value( | ||
| 343 | "grasp server(s)", | ||
| 344 | "grasp server", | ||
| 345 | default_choices, | ||
| 346 | selections, | ||
| 347 | normalize_grasp_server_url, | ||
| 348 | )?; | ||
| 349 | if grasp_servers.is_empty() { | ||
| 350 | // ask again | ||
| 351 | continue; | ||
| 352 | } | ||
| 353 | let normalised_grasp_servers: Vec<String> = grasp_servers | ||
| 354 | .iter() | ||
| 355 | .filter_map(|g| normalize_grasp_server_url(g).ok()) | ||
| 356 | .collect(); | ||
| 357 | // if any grasp servers not listed in user grasp list prompt to update | ||
| 358 | let grasp_servers_not_in_user_prefs: Vec<String> = normalised_grasp_servers | ||
| 359 | .iter() | ||
| 360 | .filter(|g| { | ||
| 361 | !user_ref.grasp_list.urls.contains( | ||
| 362 | // unwrap is safe as we constructed g | ||
| 363 | &nostr::Url::parse(&format_grasp_server_url_as_relay_url(g).unwrap()) | ||
| 364 | .unwrap(), | ||
| 365 | ) | ||
| 366 | }) | ||
| 367 | .cloned() | ||
| 368 | .collect(); | ||
| 369 | if !grasp_servers_not_in_user_prefs.is_empty() | ||
| 370 | && Interactor::default().confirm( | ||
| 371 | PromptConfirmParms::default() | ||
| 372 | .with_prompt( | ||
| 373 | "add these to your list of prefered grasp servers?".to_string(), | ||
| 374 | ) | ||
| 375 | .with_default(true), | ||
| 245 | )? | 376 | )? |
| 246 | .clone(); | 377 | { |
| 247 | if !git_ref_or_branch_name.starts_with("refs/") { | 378 | for g in &normalised_grasp_servers { |
| 248 | git_ref_or_branch_name = format!("refs/heads/{git_ref_or_branch_name}"); | 379 | let as_url = nostr::Url::parse(&format_grasp_server_url_as_relay_url(g)?)?; |
| 380 | if !user_ref.grasp_list.urls.contains(&as_url) { | ||
| 381 | user_ref.grasp_list.urls.push(as_url); | ||
| 382 | } | ||
| 383 | } | ||
| 384 | new_grasp_server_events.push(user_ref.grasp_list.to_event(&signer).await?); | ||
| 249 | } | 385 | } |
| 250 | git_ref = Some(git_ref_or_branch_name); | 386 | normalised_grasp_servers |
| 251 | } else { | 387 | } else { |
| 252 | println!("invalid clone url"); | 388 | println!( |
| 389 | "{} personal-fork so we can push commits to your prefered grasp servers", | ||
| 390 | if user_repo_ref.is_some() { | ||
| 391 | "Updating" | ||
| 392 | } else { | ||
| 393 | "Creating a" | ||
| 394 | }, | ||
| 395 | ); | ||
| 396 | untried_user_grasp_servers | ||
| 397 | }; | ||
| 398 | |||
| 399 | let grasp_servers_as_personal_clone_url: Vec<String> = grasp_servers | ||
| 400 | .iter() | ||
| 401 | .filter_map(|g| { | ||
| 402 | format_grasp_server_url_as_clone_url( | ||
| 403 | g, | ||
| 404 | &user_ref.public_key, | ||
| 405 | &repo_ref.identifier, | ||
| 406 | ) | ||
| 407 | .ok() | ||
| 408 | }) | ||
| 409 | .collect(); | ||
| 410 | |||
| 411 | // create personal-fork / update existing user repo and add these grasp servers | ||
| 412 | let updated_user_repo_ref = { | ||
| 413 | if let Some(mut user_repo_ref) = user_repo_ref { | ||
| 414 | for g in &grasp_servers_as_personal_clone_url { | ||
| 415 | let _ = user_repo_ref.add_grasp_server(g); | ||
| 416 | } | ||
| 417 | user_repo_ref | ||
| 418 | } else { | ||
| 419 | // clone repo_ref and reset as personal-fork | ||
| 420 | let mut user_repo_ref = repo_ref.clone(); | ||
| 421 | user_repo_ref.trusted_maintainer = user_ref.public_key; | ||
| 422 | user_repo_ref.maintainers = vec![user_ref.public_key]; | ||
| 423 | user_repo_ref.git_server = vec![]; | ||
| 424 | user_repo_ref.relays = vec![]; | ||
| 425 | if !user_repo_ref | ||
| 426 | .hashtags | ||
| 427 | .contains(&"personal-fork".to_string()) | ||
| 428 | { | ||
| 429 | user_repo_ref.hashtags.push("personal-fork".to_string()); | ||
| 430 | } | ||
| 431 | user_repo_ref | ||
| 432 | } | ||
| 433 | }; | ||
| 434 | // pubish event to my-relays and my-fork-relays | ||
| 435 | new_grasp_server_events.push(updated_user_repo_ref.to_event(&signer).await?); | ||
| 436 | send_events( | ||
| 437 | &client, | ||
| 438 | Some(git_repo_path), | ||
| 439 | new_grasp_server_events, | ||
| 440 | user_ref.relays.write(), | ||
| 441 | updated_user_repo_ref.relays.clone(), | ||
| 442 | !cli_args.disable_cli_spinners, | ||
| 443 | false, | ||
| 444 | ) | ||
| 445 | .await?; | ||
| 446 | user_repo_ref = Some(updated_user_repo_ref); | ||
| 447 | // wait a few seconds | ||
| 448 | let countdown_start = 5; | ||
| 449 | let term = console::Term::stdout(); | ||
| 450 | for i in (1..=countdown_start).rev() { | ||
| 451 | term.write_line( | ||
| 452 | format!( | ||
| 453 | "waiting {i}s grasp servers to create your repo before we push your data" | ||
| 454 | ) | ||
| 455 | .as_str(), | ||
| 456 | )?; | ||
| 457 | thread::sleep(Duration::new(1, 0)); // Sleep for 1 second | ||
| 458 | term.clear_last_lines(1)?; | ||
| 459 | } | ||
| 460 | term.flush().unwrap(); // Ensure the output is flushed to the terminal | ||
| 461 | |||
| 462 | // add grasp servers to to_try | ||
| 463 | for url in grasp_servers_as_personal_clone_url { | ||
| 464 | to_try.push(url); | ||
| 253 | } | 465 | } |
| 466 | // the loop with continue with the grasp servers | ||
| 254 | } | 467 | } |
| 255 | } else { | 468 | } else { |
| 256 | let events = generate_cover_letter_and_patch_events( | 469 | let events = generate_cover_letter_and_patch_events( |
diff --git a/src/lib/cli_interactor.rs b/src/lib/cli_interactor.rs index 8fca81d..8bcda19 100644 --- a/src/lib/cli_interactor.rs +++ b/src/lib/cli_interactor.rs | |||
| @@ -236,6 +236,93 @@ impl PromptMultiChoiceParms { | |||
| 236 | } | 236 | } |
| 237 | } | 237 | } |
| 238 | 238 | ||
| 239 | pub fn multi_select_with_custom_value<F>( | ||
| 240 | prompt: &str, | ||
| 241 | custom_choice_prompt: &str, | ||
| 242 | mut choices: Vec<String>, | ||
| 243 | mut defaults: Vec<bool>, | ||
| 244 | validate_choice: F, | ||
| 245 | ) -> Result<Vec<String>> | ||
| 246 | where | ||
| 247 | F: Fn(&str) -> Result<String>, | ||
| 248 | { | ||
| 249 | let mut selected_choices = vec![]; | ||
| 250 | |||
| 251 | // Loop to allow users to add more choices | ||
| 252 | loop { | ||
| 253 | // Add 'add another' option at the end of the choices | ||
| 254 | let mut current_choices = choices.clone(); | ||
| 255 | current_choices.push(if current_choices.is_empty() { | ||
| 256 | "add".to_string() | ||
| 257 | } else { | ||
| 258 | "add another".to_string() | ||
| 259 | }); | ||
| 260 | |||
| 261 | // Create default selections based on the provided defaults | ||
| 262 | let mut current_defaults = defaults.clone(); | ||
| 263 | current_defaults.push(current_choices.len() == 1); // 'add another' should not be selected by default | ||
| 264 | |||
| 265 | // Prompt for selections | ||
| 266 | let selected_indices: Vec<usize> = Interactor::default().multi_choice( | ||
| 267 | PromptMultiChoiceParms::default() | ||
| 268 | .with_prompt(prompt) | ||
| 269 | .dont_report() | ||
| 270 | .with_choices(current_choices.clone()) | ||
| 271 | .with_defaults(current_defaults), | ||
| 272 | )?; | ||
| 273 | |||
| 274 | // Collect selected choices | ||
| 275 | selected_choices.clear(); // Clear previous selections to update | ||
| 276 | for &index in &selected_indices { | ||
| 277 | if index < choices.len() { | ||
| 278 | // Exclude 'add another' option | ||
| 279 | selected_choices.push(choices[index].clone()); | ||
| 280 | } | ||
| 281 | } | ||
| 282 | |||
| 283 | // Check if 'add another' was selected | ||
| 284 | if selected_indices.contains(&(choices.len())) { | ||
| 285 | // Last index is 'add another' | ||
| 286 | let mut new_choice: String; | ||
| 287 | loop { | ||
| 288 | new_choice = Interactor::default().input( | ||
| 289 | PromptInputParms::default() | ||
| 290 | .with_prompt(custom_choice_prompt) | ||
| 291 | .dont_report() | ||
| 292 | .optional(), | ||
| 293 | )?; | ||
| 294 | |||
| 295 | if new_choice.is_empty() { | ||
| 296 | break; | ||
| 297 | } | ||
| 298 | // Validate the new choice | ||
| 299 | match validate_choice(&new_choice) { | ||
| 300 | Ok(valid_choice) => { | ||
| 301 | new_choice = valid_choice; // Use the fixed version of the input | ||
| 302 | break; // Valid choice, exit the loop | ||
| 303 | } | ||
| 304 | Err(err) => { | ||
| 305 | // Inform the user about the validation error | ||
| 306 | println!("Error: {err}"); | ||
| 307 | } | ||
| 308 | } | ||
| 309 | } | ||
| 310 | |||
| 311 | // Add the new choice to the choices vector | ||
| 312 | if !new_choice.is_empty() { | ||
| 313 | choices.push(new_choice.clone()); // Add new choice to the end of the list | ||
| 314 | selected_choices.push(new_choice); // Automatically select the new choice | ||
| 315 | defaults.push(true); // Set the new choice as selected by default | ||
| 316 | } | ||
| 317 | } else { | ||
| 318 | // Exit the loop if 'add another' was not selected | ||
| 319 | break; | ||
| 320 | } | ||
| 321 | } | ||
| 322 | |||
| 323 | Ok(selected_choices) | ||
| 324 | } | ||
| 325 | |||
| 239 | #[derive(Debug, Default)] | 326 | #[derive(Debug, Default)] |
| 240 | pub struct Printer { | 327 | pub struct Printer { |
| 241 | printed_lines: Vec<String>, | 328 | printed_lines: Vec<String>, |
diff --git a/src/lib/client.rs b/src/lib/client.rs index b27f9b1..9ce3e24 100644 --- a/src/lib/client.rs +++ b/src/lib/client.rs | |||
| @@ -53,7 +53,7 @@ use crate::{ | |||
| 53 | get_dirs, | 53 | get_dirs, |
| 54 | git::{Repo, RepoActions, get_git_config_item}, | 54 | git::{Repo, RepoActions, get_git_config_item}, |
| 55 | git_events::{ | 55 | git_events::{ |
| 56 | KIND_PULL_REQUEST, KIND_PULL_REQUEST_UPDATE, event_is_cover_letter, | 56 | KIND_PULL_REQUEST, KIND_PULL_REQUEST_UPDATE, KIND_USER_GRASP_LIST, event_is_cover_letter, |
| 57 | event_is_patch_set_root, event_is_revision_root, event_is_valid_pr_or_pr_update, | 57 | event_is_patch_set_root, event_is_revision_root, event_is_valid_pr_or_pr_update, |
| 58 | status_kinds, | 58 | status_kinds, |
| 59 | }, | 59 | }, |
| @@ -233,7 +233,7 @@ impl Connect for Client { | |||
| 233 | if let Some(git_repo_path) = git_repo_path { | 233 | if let Some(git_repo_path) = git_repo_path { |
| 234 | save_event_in_local_cache(git_repo_path, &event).await?; | 234 | save_event_in_local_cache(git_repo_path, &event).await?; |
| 235 | } | 235 | } |
| 236 | if event.kind.eq(&Kind::GitRepoAnnouncement) { | 236 | if [Kind::GitRepoAnnouncement, KIND_USER_GRASP_LIST].contains(&event.kind) { |
| 237 | save_event_in_global_cache(git_repo_path, &event).await?; | 237 | save_event_in_global_cache(git_repo_path, &event).await?; |
| 238 | } | 238 | } |
| 239 | Ok(event.id) | 239 | Ok(event.id) |
| @@ -1310,17 +1310,21 @@ async fn create_relays_request( | |||
| 1310 | user_profiles.insert(current_user); | 1310 | user_profiles.insert(current_user); |
| 1311 | } | 1311 | } |
| 1312 | } | 1312 | } |
| 1313 | let mut map: HashMap<PublicKey, (Timestamp, Timestamp)> = HashMap::new(); | 1313 | let mut map: HashMap<PublicKey, (Timestamp, Timestamp, Timestamp)> = HashMap::new(); |
| 1314 | for public_key in &user_profiles { | 1314 | for public_key in &user_profiles { |
| 1315 | if let Ok(user_ref) = get_user_ref_from_cache(git_repo_path, public_key).await { | 1315 | if let Ok(user_ref) = get_user_ref_from_cache(git_repo_path, public_key).await { |
| 1316 | map.insert( | 1316 | map.insert( |
| 1317 | public_key.to_owned(), | 1317 | public_key.to_owned(), |
| 1318 | (user_ref.metadata.created_at, user_ref.relays.created_at), | 1318 | ( |
| 1319 | user_ref.metadata.created_at, | ||
| 1320 | user_ref.relays.created_at, | ||
| 1321 | user_ref.grasp_list.created_at, | ||
| 1322 | ), | ||
| 1319 | ); | 1323 | ); |
| 1320 | } else { | 1324 | } else { |
| 1321 | map.insert( | 1325 | map.insert( |
| 1322 | public_key.to_owned(), | 1326 | public_key.to_owned(), |
| 1323 | (Timestamp::from(0), Timestamp::from(0)), | 1327 | (Timestamp::from(0), Timestamp::from(0), Timestamp::from(0)), |
| 1324 | ); | 1328 | ); |
| 1325 | } | 1329 | } |
| 1326 | } | 1330 | } |
| @@ -1547,16 +1551,22 @@ async fn process_fetched_events( | |||
| 1547 | { | 1551 | { |
| 1548 | fresh_profiles.insert(event.pubkey); | 1552 | fresh_profiles.insert(event.pubkey); |
| 1549 | } | 1553 | } |
| 1550 | } else if [Kind::RelayList, Kind::Metadata].contains(&event.kind) { | 1554 | } else if [Kind::RelayList, Kind::Metadata, KIND_USER_GRASP_LIST].contains(&event.kind) |
| 1555 | { | ||
| 1551 | if request.missing_contributor_profiles.contains(&event.pubkey) { | 1556 | if request.missing_contributor_profiles.contains(&event.pubkey) { |
| 1552 | report.contributor_profiles.insert(event.pubkey); | 1557 | report.contributor_profiles.insert(event.pubkey); |
| 1553 | } else if let Some((_, (metadata_timestamp, relay_list_timestamp))) = request | 1558 | } else if let Some(( |
| 1559 | _, | ||
| 1560 | (metadata_timestamp, relay_list_timestamp, grasp_list_timestamp), | ||
| 1561 | )) = request | ||
| 1554 | .profiles_to_fetch_from_user_relays | 1562 | .profiles_to_fetch_from_user_relays |
| 1555 | .get_key_value(&event.pubkey) | 1563 | .get_key_value(&event.pubkey) |
| 1556 | { | 1564 | { |
| 1557 | if (Kind::Metadata.eq(&event.kind) && event.created_at.gt(metadata_timestamp)) | 1565 | if (Kind::Metadata.eq(&event.kind) && event.created_at.gt(metadata_timestamp)) |
| 1558 | || (Kind::RelayList.eq(&event.kind) | 1566 | || (Kind::RelayList.eq(&event.kind) |
| 1559 | && event.created_at.gt(relay_list_timestamp)) | 1567 | && event.created_at.gt(relay_list_timestamp)) |
| 1568 | || (KIND_USER_GRASP_LIST.eq(&event.kind) | ||
| 1569 | && event.created_at.gt(grasp_list_timestamp)) | ||
| 1560 | { | 1570 | { |
| 1561 | report.profile_updates.insert(event.pubkey); | 1571 | report.profile_updates.insert(event.pubkey); |
| 1562 | } | 1572 | } |
| @@ -1718,35 +1728,21 @@ pub fn get_filter_repo_events(repo_coordinates: &HashSet<Nip19Coordinate>) -> no | |||
| 1718 | .map(|c| c.identifier.clone()) | 1728 | .map(|c| c.identifier.clone()) |
| 1719 | .collect::<Vec<String>>(), | 1729 | .collect::<Vec<String>>(), |
| 1720 | ) | 1730 | ) |
| 1721 | .authors( | ||
| 1722 | repo_coordinates | ||
| 1723 | .iter() | ||
| 1724 | .map(|c| c.public_key) | ||
| 1725 | .collect::<Vec<PublicKey>>(), | ||
| 1726 | ) | ||
| 1727 | } | 1731 | } |
| 1728 | 1732 | ||
| 1729 | pub static STATE_KIND: nostr::Kind = Kind::Custom(30618); | 1733 | pub static STATE_KIND: nostr::Kind = Kind::Custom(30618); |
| 1730 | pub fn get_filter_state_events(repo_coordinates: &HashSet<Nip19Coordinate>) -> nostr::Filter { | 1734 | pub fn get_filter_state_events(repo_coordinates: &HashSet<Nip19Coordinate>) -> nostr::Filter { |
| 1731 | nostr::Filter::default() | 1735 | nostr::Filter::default().kind(STATE_KIND).identifiers( |
| 1732 | .kind(STATE_KIND) | 1736 | repo_coordinates |
| 1733 | .identifiers( | 1737 | .iter() |
| 1734 | repo_coordinates | 1738 | .map(|c| c.identifier.clone()) |
| 1735 | .iter() | 1739 | .collect::<Vec<String>>(), |
| 1736 | .map(|c| c.identifier.clone()) | 1740 | ) |
| 1737 | .collect::<Vec<String>>(), | ||
| 1738 | ) | ||
| 1739 | .authors( | ||
| 1740 | repo_coordinates | ||
| 1741 | .iter() | ||
| 1742 | .map(|c| c.public_key) | ||
| 1743 | .collect::<Vec<PublicKey>>(), | ||
| 1744 | ) | ||
| 1745 | } | 1741 | } |
| 1746 | 1742 | ||
| 1747 | pub fn get_filter_contributor_profiles(contributors: HashSet<PublicKey>) -> nostr::Filter { | 1743 | pub fn get_filter_contributor_profiles(contributors: HashSet<PublicKey>) -> nostr::Filter { |
| 1748 | nostr::Filter::default() | 1744 | nostr::Filter::default() |
| 1749 | .kinds(vec![Kind::Metadata, Kind::RelayList]) | 1745 | .kinds(vec![Kind::Metadata, Kind::RelayList, KIND_USER_GRASP_LIST]) |
| 1750 | .authors(contributors) | 1746 | .authors(contributors) |
| 1751 | } | 1747 | } |
| 1752 | 1748 | ||
| @@ -1850,7 +1846,7 @@ pub struct FetchRequest { | |||
| 1850 | contributors: HashSet<PublicKey>, | 1846 | contributors: HashSet<PublicKey>, |
| 1851 | missing_contributor_profiles: HashSet<PublicKey>, | 1847 | missing_contributor_profiles: HashSet<PublicKey>, |
| 1852 | existing_events: HashSet<EventId>, | 1848 | existing_events: HashSet<EventId>, |
| 1853 | profiles_to_fetch_from_user_relays: HashMap<PublicKey, (Timestamp, Timestamp)>, | 1849 | profiles_to_fetch_from_user_relays: HashMap<PublicKey, (Timestamp, Timestamp, Timestamp)>, |
| 1854 | user_relays_for_profiles: HashSet<RelayUrl>, | 1850 | user_relays_for_profiles: HashSet<RelayUrl>, |
| 1855 | } | 1851 | } |
| 1856 | 1852 | ||
diff --git a/src/lib/git_events.rs b/src/lib/git_events.rs index bbfcbea..76c31de 100644 --- a/src/lib/git_events.rs +++ b/src/lib/git_events.rs | |||
| @@ -63,6 +63,7 @@ pub fn status_kinds() -> Vec<Kind> { | |||
| 63 | 63 | ||
| 64 | pub const KIND_PULL_REQUEST: Kind = Kind::Custom(1618); | 64 | pub const KIND_PULL_REQUEST: Kind = Kind::Custom(1618); |
| 65 | pub const KIND_PULL_REQUEST_UPDATE: Kind = Kind::Custom(1619); | 65 | pub const KIND_PULL_REQUEST_UPDATE: Kind = Kind::Custom(1619); |
| 66 | pub const KIND_USER_GRASP_LIST: Kind = Kind::Custom(10317); | ||
| 66 | 67 | ||
| 67 | pub fn event_is_patch_set_root(event: &Event) -> bool { | 68 | pub fn event_is_patch_set_root(event: &Event) -> bool { |
| 68 | event.kind.eq(&Kind::GitPatch) | 69 | event.kind.eq(&Kind::GitPatch) |
diff --git a/src/lib/login/user.rs b/src/lib/login/user.rs index 071cb25..0b702ef 100644 --- a/src/lib/login/user.rs +++ b/src/lib/login/user.rs | |||
| @@ -1,7 +1,7 @@ | |||
| 1 | use std::{collections::HashSet, path::Path}; | 1 | use std::{collections::HashSet, path::Path, sync::Arc}; |
| 2 | 2 | ||
| 3 | use anyhow::{Context, Result, bail}; | 3 | use anyhow::{Context, Result, bail}; |
| 4 | use nostr::PublicKey; | 4 | use nostr::{PublicKey, Url, event::Tag, signer::NostrSigner}; |
| 5 | use nostr_sdk::{Alphabet, JsonUtil, Kind, SingleLetterTag, Timestamp, ToBech32}; | 5 | use nostr_sdk::{Alphabet, JsonUtil, Kind, SingleLetterTag, Timestamp, ToBech32}; |
| 6 | use serde::{self, Deserialize, Serialize}; | 6 | use serde::{self, Deserialize, Serialize}; |
| 7 | 7 | ||
| @@ -9,13 +9,17 @@ use serde::{self, Deserialize, Serialize}; | |||
| 9 | use crate::client::Client; | 9 | use crate::client::Client; |
| 10 | #[cfg(test)] | 10 | #[cfg(test)] |
| 11 | use crate::client::MockConnect; | 11 | use crate::client::MockConnect; |
| 12 | use crate::client::{Connect, get_event_from_global_cache}; | 12 | use crate::{ |
| 13 | client::{Connect, get_event_from_global_cache, sign_event}, | ||
| 14 | git_events::KIND_USER_GRASP_LIST, | ||
| 15 | }; | ||
| 13 | 16 | ||
| 14 | #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] | 17 | #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] |
| 15 | pub struct UserRef { | 18 | pub struct UserRef { |
| 16 | pub public_key: PublicKey, | 19 | pub public_key: PublicKey, |
| 17 | pub metadata: UserMetadata, | 20 | pub metadata: UserMetadata, |
| 18 | pub relays: UserRelays, | 21 | pub relays: UserRelays, |
| 22 | pub grasp_list: UserGraspList, | ||
| 19 | } | 23 | } |
| 20 | 24 | ||
| 21 | #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] | 25 | #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] |
| @@ -49,6 +53,35 @@ impl UserRelays { | |||
| 49 | } | 53 | } |
| 50 | 54 | ||
| 51 | #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] | 55 | #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] |
| 56 | pub struct UserGraspList { | ||
| 57 | pub urls: Vec<Url>, | ||
| 58 | pub created_at: Timestamp, | ||
| 59 | } | ||
| 60 | |||
| 61 | impl UserGraspList { | ||
| 62 | pub async fn to_event(&mut self, signer: &Arc<dyn NostrSigner>) -> Result<nostr::Event> { | ||
| 63 | let event = sign_event( | ||
| 64 | nostr_sdk::EventBuilder::new(KIND_USER_GRASP_LIST, "").tags( | ||
| 65 | self.urls | ||
| 66 | .iter() | ||
| 67 | .map(|url| { | ||
| 68 | Tag::custom( | ||
| 69 | nostr::TagKind::Custom(std::borrow::Cow::Borrowed("g")), | ||
| 70 | vec![url.to_string()], | ||
| 71 | ) | ||
| 72 | }) | ||
| 73 | .collect::<Vec<_>>(), | ||
| 74 | ), | ||
| 75 | signer, | ||
| 76 | "user grasp list".to_string(), | ||
| 77 | ) | ||
| 78 | .await?; | ||
| 79 | self.created_at = event.created_at; | ||
| 80 | Ok(event) | ||
| 81 | } | ||
| 82 | } | ||
| 83 | |||
| 84 | #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] | ||
| 52 | pub struct UserRelayRef { | 85 | pub struct UserRelayRef { |
| 53 | pub url: String, | 86 | pub url: String, |
| 54 | pub read: bool, | 87 | pub read: bool, |
| @@ -84,6 +117,7 @@ pub async fn get_user_details( | |||
| 84 | public_key: public_key.to_owned(), | 117 | public_key: public_key.to_owned(), |
| 85 | metadata: extract_user_metadata(public_key, &[])?, | 118 | metadata: extract_user_metadata(public_key, &[])?, |
| 86 | relays: extract_user_relays(public_key, &[]), | 119 | relays: extract_user_relays(public_key, &[]), |
| 120 | grasp_list: extract_user_grasp_list(public_key, &[]), | ||
| 87 | }; | 121 | }; |
| 88 | if cache_only { | 122 | if cache_only { |
| 89 | Ok(empty) | 123 | Ok(empty) |
| @@ -117,6 +151,9 @@ pub async fn get_user_ref_from_cache( | |||
| 117 | nostr::Filter::default() | 151 | nostr::Filter::default() |
| 118 | .author(*public_key) | 152 | .author(*public_key) |
| 119 | .kind(Kind::RelayList), | 153 | .kind(Kind::RelayList), |
| 154 | nostr::Filter::default() | ||
| 155 | .author(*public_key) | ||
| 156 | .kind(KIND_USER_GRASP_LIST), | ||
| 120 | ]; | 157 | ]; |
| 121 | 158 | ||
| 122 | let events = get_event_from_global_cache(git_repo_path, filters.clone()).await?; | 159 | let events = get_event_from_global_cache(git_repo_path, filters.clone()).await?; |
| @@ -128,6 +165,7 @@ pub async fn get_user_ref_from_cache( | |||
| 128 | public_key: public_key.to_owned(), | 165 | public_key: public_key.to_owned(), |
| 129 | metadata: extract_user_metadata(public_key, &events)?, | 166 | metadata: extract_user_metadata(public_key, &events)?, |
| 130 | relays: extract_user_relays(public_key, &events), | 167 | relays: extract_user_relays(public_key, &events), |
| 168 | grasp_list: extract_user_grasp_list(public_key, &events), | ||
| 131 | }) | 169 | }) |
| 132 | } | 170 | } |
| 133 | 171 | ||
| @@ -215,3 +253,36 @@ pub fn extract_user_relays(public_key: &nostr::PublicKey, events: &[nostr::Event | |||
| 215 | }, | 253 | }, |
| 216 | } | 254 | } |
| 217 | } | 255 | } |
| 256 | |||
| 257 | pub fn extract_user_grasp_list( | ||
| 258 | public_key: &nostr::PublicKey, | ||
| 259 | events: &[nostr::Event], | ||
| 260 | ) -> UserGraspList { | ||
| 261 | let event = events | ||
| 262 | .iter() | ||
| 263 | .filter(|e| e.kind.eq(&KIND_USER_GRASP_LIST) && e.pubkey.eq(public_key)) | ||
| 264 | .max_by_key(|e| e.created_at); | ||
| 265 | |||
| 266 | UserGraspList { | ||
| 267 | urls: if let Some(event) = event { | ||
| 268 | event | ||
| 269 | .tags | ||
| 270 | .iter() | ||
| 271 | .filter_map(|t| { | ||
| 272 | if t.as_slice().len() > 1 && t.as_slice()[0] == "g" { | ||
| 273 | Url::parse(&t.as_slice()[1]).ok() | ||
| 274 | } else { | ||
| 275 | None | ||
| 276 | } | ||
| 277 | }) | ||
| 278 | .collect() | ||
| 279 | } else { | ||
| 280 | vec![] | ||
| 281 | }, | ||
| 282 | created_at: if let Some(event) = event { | ||
| 283 | event.created_at | ||
| 284 | } else { | ||
| 285 | Timestamp::from(0) | ||
| 286 | }, | ||
| 287 | } | ||
| 288 | } | ||
diff --git a/src/lib/repo_ref.rs b/src/lib/repo_ref.rs index a3e1317..e3f71a1 100644 --- a/src/lib/repo_ref.rs +++ b/src/lib/repo_ref.rs | |||
| @@ -40,6 +40,7 @@ pub struct RepoRef { | |||
| 40 | pub web: Vec<String>, | 40 | pub web: Vec<String>, |
| 41 | pub relays: Vec<RelayUrl>, | 41 | pub relays: Vec<RelayUrl>, |
| 42 | pub blossoms: Vec<Url>, | 42 | pub blossoms: Vec<Url>, |
| 43 | pub hashtags: Vec<String>, | ||
| 43 | pub maintainers: Vec<PublicKey>, | 44 | pub maintainers: Vec<PublicKey>, |
| 44 | pub trusted_maintainer: PublicKey, | 45 | pub trusted_maintainer: PublicKey, |
| 45 | // set to None if not known | 46 | // set to None if not known |
| @@ -71,6 +72,7 @@ impl TryFrom<(nostr::Event, Option<PublicKey>)> for RepoRef { | |||
| 71 | web: Vec::new(), | 72 | web: Vec::new(), |
| 72 | relays: Vec::new(), | 73 | relays: Vec::new(), |
| 73 | blossoms: Vec::new(), | 74 | blossoms: Vec::new(), |
| 75 | hashtags: Vec::new(), | ||
| 74 | maintainers: Vec::new(), | 76 | maintainers: Vec::new(), |
| 75 | trusted_maintainer: trusted_maintainer.unwrap_or(event.pubkey), | 77 | trusted_maintainer: trusted_maintainer.unwrap_or(event.pubkey), |
| 76 | maintainers_without_annoucnement: None, | 78 | maintainers_without_annoucnement: None, |
| @@ -118,6 +120,7 @@ impl TryFrom<(nostr::Event, Option<PublicKey>)> for RepoRef { | |||
| 118 | } | 120 | } |
| 119 | } | 121 | } |
| 120 | } | 122 | } |
| 123 | [t, hashtag, ..] if t == "t" => r.hashtags.push(hashtag.clone()), | ||
| 121 | [t, blossoms @ ..] if t == "blossoms" => { | 124 | [t, blossoms @ ..] if t == "blossoms" => { |
| 122 | for b in blossoms { | 125 | for b in blossoms { |
| 123 | if let Ok(b) = Url::parse(b) { | 126 | if let Ok(b) = Url::parse(b) { |
| @@ -217,6 +220,15 @@ impl RepoRef { | |||
| 217 | vec![format!("git repository: {}", self.name.clone())], | 220 | vec![format!("git repository: {}", self.name.clone())], |
| 218 | ), | 221 | ), |
| 219 | ], | 222 | ], |
| 223 | self.hashtags | ||
| 224 | .iter() | ||
| 225 | .map(|h| { | ||
| 226 | Tag::custom( | ||
| 227 | nostr::TagKind::Custom(std::borrow::Cow::Borrowed("t")), | ||
| 228 | vec![h.clone()], | ||
| 229 | ) | ||
| 230 | }) | ||
| 231 | .collect(), | ||
| 220 | if self.blossoms.is_empty() { | 232 | if self.blossoms.is_empty() { |
| 221 | vec![] | 233 | vec![] |
| 222 | } else { | 234 | } else { |
| @@ -311,6 +323,34 @@ impl RepoRef { | |||
| 311 | pub fn grasp_servers(&self) -> Vec<String> { | 323 | pub fn grasp_servers(&self) -> Vec<String> { |
| 312 | detect_existing_grasp_servers(Some(self), &[], &[], &self.identifier) | 324 | detect_existing_grasp_servers(Some(self), &[], &[], &self.identifier) |
| 313 | } | 325 | } |
| 326 | |||
| 327 | // returns false if already present so didn't need adding | ||
| 328 | pub fn add_grasp_server(&mut self, clone_url: &str) -> Result<bool> { | ||
| 329 | if !clone_url.starts_with("http") { | ||
| 330 | bail!("invalid grasp server clone url"); | ||
| 331 | } | ||
| 332 | extract_npub(clone_url) | ||
| 333 | .context("invalid grasp server clone url. does not contain valid npub")?; | ||
| 334 | if !(clone_url.ends_with(".git") || clone_url.ends_with(".git/")) { | ||
| 335 | bail!("invalid grasp server clone url. does not end with .git"); | ||
| 336 | } | ||
| 337 | |||
| 338 | let relay_url = RelayUrl::parse( | ||
| 339 | &format_grasp_server_url_as_relay_url(clone_url) | ||
| 340 | .context("invalid grasp server clone url")?, | ||
| 341 | ) | ||
| 342 | .context("invalid grasp server clone url")?; | ||
| 343 | |||
| 344 | if !self.relays.contains(&relay_url) { | ||
| 345 | self.relays.push(relay_url); | ||
| 346 | } | ||
| 347 | if !self.git_server.contains(&clone_url.to_string()) { | ||
| 348 | self.git_server.push(clone_url.to_string()); | ||
| 349 | Ok(true) | ||
| 350 | } else { | ||
| 351 | Ok(false) | ||
| 352 | } | ||
| 353 | } | ||
| 314 | } | 354 | } |
| 315 | 355 | ||
| 316 | pub async fn get_repo_coordinates_when_remote_unknown( | 356 | pub async fn get_repo_coordinates_when_remote_unknown( |
| @@ -699,13 +739,49 @@ pub fn extract_npub(s: &str) -> Result<&str> { | |||
| 699 | } | 739 | } |
| 700 | } | 740 | } |
| 701 | 741 | ||
| 742 | // this should be called is_grasp_server_in_list | ||
| 702 | pub fn is_grasp_server(url: &str, grasp_servers: &[String]) -> bool { | 743 | pub fn is_grasp_server(url: &str, grasp_servers: &[String]) -> bool { |
| 703 | if !grasp_servers.is_empty() { | 744 | if !grasp_servers.is_empty() { |
| 704 | if let Ok(n) = normalize_grasp_server_url(url) { | 745 | if let Ok(url) = normalize_grasp_server_url(url) { |
| 705 | return grasp_servers.contains(&n); | 746 | grasp_servers.iter().any(|s| { |
| 747 | if let Ok(s) = normalize_grasp_server_url(s) { | ||
| 748 | s == url | ||
| 749 | } else { | ||
| 750 | false | ||
| 751 | } | ||
| 752 | }) | ||
| 753 | } else { | ||
| 754 | false | ||
| 706 | } | 755 | } |
| 756 | } else { | ||
| 757 | false | ||
| 758 | } | ||
| 759 | } | ||
| 760 | |||
| 761 | pub fn format_grasp_server_url_as_relay_url(url: &str) -> Result<String> { | ||
| 762 | let grasp_server_url = normalize_grasp_server_url(url)?; | ||
| 763 | if grasp_server_url.contains("http://") { | ||
| 764 | return Ok(grasp_server_url.replace("http://", "ws://")); | ||
| 707 | } | 765 | } |
| 708 | false | 766 | Ok(format!("wss://{grasp_server_url}")) |
| 767 | } | ||
| 768 | |||
| 769 | pub fn format_grasp_server_url_as_clone_url( | ||
| 770 | grasp_server: &str, | ||
| 771 | public_key: &PublicKey, | ||
| 772 | identifier: &str, | ||
| 773 | ) -> Result<String> { | ||
| 774 | let grasp_server_url = normalize_grasp_server_url(grasp_server)?; | ||
| 775 | |||
| 776 | let prefix = if grasp_server_url.contains("http://") { | ||
| 777 | "" | ||
| 778 | } else { | ||
| 779 | "https://" | ||
| 780 | }; | ||
| 781 | Ok(format!( | ||
| 782 | "{prefix}{grasp_server_url}/{}/{identifier}.git", | ||
| 783 | public_key.to_bech32()? | ||
| 784 | )) | ||
| 709 | } | 785 | } |
| 710 | 786 | ||
| 711 | #[cfg(test)] | 787 | #[cfg(test)] |
| @@ -730,6 +806,7 @@ mod tests { | |||
| 730 | RelayUrl::parse("ws://relay2.io").unwrap(), | 806 | RelayUrl::parse("ws://relay2.io").unwrap(), |
| 731 | ], | 807 | ], |
| 732 | blossoms: vec![], | 808 | blossoms: vec![], |
| 809 | hashtags: vec![], | ||
| 733 | trusted_maintainer: TEST_KEY_1_KEYS.public_key(), | 810 | trusted_maintainer: TEST_KEY_1_KEYS.public_key(), |
| 734 | maintainers_without_annoucnement: None, | 811 | maintainers_without_annoucnement: None, |
| 735 | maintainers: vec![TEST_KEY_1_KEYS.public_key(), TEST_KEY_2_KEYS.public_key()], | 812 | maintainers: vec![TEST_KEY_1_KEYS.public_key(), TEST_KEY_2_KEYS.public_key()], |