diff options
| author | DanConwayDev <DanConwayDev@protonmail.com> | 2025-08-06 12:52:59 +0100 |
|---|---|---|
| committer | DanConwayDev <DanConwayDev@protonmail.com> | 2025-08-07 17:25:50 +0100 |
| commit | a9b2ebf8216be34950e54dd9a446dbdc0c9c744a (patch) | |
| tree | 5a103933852fbcfcd42b13716cb92eeca5325d6d /src/lib | |
| parent | 29f61ffdf155ea88b8d9aec23d28cf70baba577e (diff) | |
feat(send): PR fallback to user / custom grasp
if use is maintainer, push PR to all repo git servers.
if user has a fork, push to all git servers it lists, and repo
grasp servers.
if user hasn't got a fork but has a user grasp list and pushing
push to repo grasp servers fails, create a personal-fork
automatically at each user grasp server and push there.
fallback to prompting user for either grasp servers or git server
with write permission.
if user provides grasp servers, suggesting adding to user preference
list.
Diffstat (limited to 'src/lib')
| -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 |
5 files changed, 267 insertions, 35 deletions
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()], |