diff options
| -rw-r--r-- | src/lib/client.rs | 118 |
1 files changed, 101 insertions, 17 deletions
diff --git a/src/lib/client.rs b/src/lib/client.rs index 41e5379..62db8d2 100644 --- a/src/lib/client.rs +++ b/src/lib/client.rs | |||
| @@ -787,6 +787,7 @@ impl Connect for Client { | |||
| 787 | fresh_coordinates.insert(c); | 787 | fresh_coordinates.insert(c); |
| 788 | } | 788 | } |
| 789 | let mut fresh_proposal_roots = request.proposals.clone(); | 789 | let mut fresh_proposal_roots = request.proposals.clone(); |
| 790 | let mut fresh_issue_roots = request.issue_ids.clone(); | ||
| 790 | let mut fresh_profiles: HashSet<PublicKey> = request | 791 | let mut fresh_profiles: HashSet<PublicKey> = request |
| 791 | .missing_contributor_profiles | 792 | .missing_contributor_profiles |
| 792 | .union( | 793 | .union( |
| @@ -819,6 +820,7 @@ impl Connect for Client { | |||
| 819 | let filters = get_fetch_filters( | 820 | let filters = get_fetch_filters( |
| 820 | &fresh_coordinates, | 821 | &fresh_coordinates, |
| 821 | &fresh_proposal_roots, | 822 | &fresh_proposal_roots, |
| 823 | &fresh_issue_roots, | ||
| 822 | &fresh_non_proposal_event_ids, | 824 | &fresh_non_proposal_event_ids, |
| 823 | &fresh_profiles, | 825 | &fresh_profiles, |
| 824 | ); | 826 | ); |
| @@ -842,6 +844,7 @@ impl Connect for Client { | |||
| 842 | 844 | ||
| 843 | fresh_coordinates = HashSet::new(); | 845 | fresh_coordinates = HashSet::new(); |
| 844 | fresh_proposal_roots = HashSet::new(); | 846 | fresh_proposal_roots = HashSet::new(); |
| 847 | fresh_issue_roots = HashSet::new(); | ||
| 845 | fresh_profiles = HashSet::new(); | 848 | fresh_profiles = HashSet::new(); |
| 846 | 849 | ||
| 847 | let relay = self.client.relay(&relay_url).await?; | 850 | let relay = self.client.relay(&relay_url).await?; |
| @@ -881,6 +884,7 @@ impl Connect for Client { | |||
| 881 | git_repo_path, | 884 | git_repo_path, |
| 882 | &mut fresh_coordinates, | 885 | &mut fresh_coordinates, |
| 883 | &mut fresh_proposal_roots, | 886 | &mut fresh_proposal_roots, |
| 887 | &mut fresh_issue_roots, | ||
| 884 | &mut fresh_profiles, | 888 | &mut fresh_profiles, |
| 885 | &mut report, | 889 | &mut report, |
| 886 | ) | 890 | ) |
| @@ -888,6 +892,7 @@ impl Connect for Client { | |||
| 888 | 892 | ||
| 889 | if fresh_coordinates.is_empty() | 893 | if fresh_coordinates.is_empty() |
| 890 | && fresh_proposal_roots.is_empty() | 894 | && fresh_proposal_roots.is_empty() |
| 895 | && fresh_issue_roots.is_empty() | ||
| 891 | && fresh_profiles.is_empty() | 896 | && fresh_profiles.is_empty() |
| 892 | { | 897 | { |
| 893 | break; | 898 | break; |
| @@ -1630,6 +1635,7 @@ async fn create_relays_request( | |||
| 1630 | }; | 1635 | }; |
| 1631 | 1636 | ||
| 1632 | let mut proposals: HashSet<EventId> = HashSet::new(); | 1637 | let mut proposals: HashSet<EventId> = HashSet::new(); |
| 1638 | let mut issue_ids: HashSet<EventId> = HashSet::new(); | ||
| 1633 | let mut missing_contributor_profiles: HashSet<PublicKey> = HashSet::new(); | 1639 | let mut missing_contributor_profiles: HashSet<PublicKey> = HashSet::new(); |
| 1634 | let mut contributors: HashSet<PublicKey> = HashSet::new(); | 1640 | let mut contributors: HashSet<PublicKey> = HashSet::new(); |
| 1635 | 1641 | ||
| @@ -1645,7 +1651,7 @@ async fn create_relays_request( | |||
| 1645 | git_repo_path, | 1651 | git_repo_path, |
| 1646 | vec![ | 1652 | vec![ |
| 1647 | nostr::Filter::default() | 1653 | nostr::Filter::default() |
| 1648 | .kinds(vec![Kind::GitPatch, KIND_PULL_REQUEST]) | 1654 | .kinds(vec![Kind::GitPatch, KIND_PULL_REQUEST, Kind::GitIssue]) |
| 1649 | .custom_tags( | 1655 | .custom_tags( |
| 1650 | SingleLetterTag::lowercase(nostr_sdk::Alphabet::A), | 1656 | SingleLetterTag::lowercase(nostr_sdk::Alphabet::A), |
| 1651 | repo_coordinates_without_relays | 1657 | repo_coordinates_without_relays |
| @@ -1663,6 +1669,9 @@ async fn create_relays_request( | |||
| 1663 | { | 1669 | { |
| 1664 | proposals.insert(event.id); | 1670 | proposals.insert(event.id); |
| 1665 | contributors.insert(event.pubkey); | 1671 | contributors.insert(event.pubkey); |
| 1672 | } else if event.kind.eq(&Kind::GitIssue) { | ||
| 1673 | issue_ids.insert(event.id); | ||
| 1674 | contributors.insert(event.pubkey); | ||
| 1666 | } | 1675 | } |
| 1667 | } | 1676 | } |
| 1668 | } | 1677 | } |
| @@ -1739,6 +1748,7 @@ async fn create_relays_request( | |||
| 1739 | for filter in get_fetch_filters( | 1748 | for filter in get_fetch_filters( |
| 1740 | &repo_coordinates_without_relays, | 1749 | &repo_coordinates_without_relays, |
| 1741 | &proposals, | 1750 | &proposals, |
| 1751 | &issue_ids, | ||
| 1742 | &HashSet::new(), /* non_proposal_event_ids not yet computed; deletion events are not | 1752 | &HashSet::new(), /* non_proposal_event_ids not yet computed; deletion events are not |
| 1743 | * cached locally */ | 1753 | * cached locally */ |
| 1744 | &missing_contributor_profiles | 1754 | &missing_contributor_profiles |
| @@ -1746,7 +1756,7 @@ async fn create_relays_request( | |||
| 1746 | &profiles_to_fetch_from_user_relays | 1756 | &profiles_to_fetch_from_user_relays |
| 1747 | .clone() | 1757 | .clone() |
| 1748 | .into_keys() | 1758 | .into_keys() |
| 1749 | .collect(), | 1759 | .collect::<HashSet<PublicKey>>(), |
| 1750 | ) | 1760 | ) |
| 1751 | .copied() | 1761 | .copied() |
| 1752 | .collect(), | 1762 | .collect(), |
| @@ -1858,6 +1868,7 @@ async fn create_relays_request( | |||
| 1858 | ids | 1868 | ids |
| 1859 | }, | 1869 | }, |
| 1860 | proposals, | 1870 | proposals, |
| 1871 | issue_ids, | ||
| 1861 | contributors, | 1872 | contributors, |
| 1862 | missing_contributor_profiles, | 1873 | missing_contributor_profiles, |
| 1863 | existing_events, | 1874 | existing_events, |
| @@ -1873,6 +1884,7 @@ async fn process_fetched_events( | |||
| 1873 | git_repo_path: Option<&Path>, | 1884 | git_repo_path: Option<&Path>, |
| 1874 | fresh_coordinates: &mut HashSet<Nip19Coordinate>, | 1885 | fresh_coordinates: &mut HashSet<Nip19Coordinate>, |
| 1875 | fresh_proposal_roots: &mut HashSet<EventId>, | 1886 | fresh_proposal_roots: &mut HashSet<EventId>, |
| 1887 | fresh_issue_roots: &mut HashSet<EventId>, | ||
| 1876 | fresh_profiles: &mut HashSet<PublicKey>, | 1888 | fresh_profiles: &mut HashSet<PublicKey>, |
| 1877 | report: &mut FetchReport, | 1889 | report: &mut FetchReport, |
| 1878 | ) -> Result<()> { | 1890 | ) -> Result<()> { |
| @@ -1976,6 +1988,14 @@ async fn process_fetched_events( | |||
| 1976 | { | 1988 | { |
| 1977 | fresh_profiles.insert(event.pubkey); | 1989 | fresh_profiles.insert(event.pubkey); |
| 1978 | } | 1990 | } |
| 1991 | } else if event.kind.eq(&Kind::GitIssue) { | ||
| 1992 | fresh_issue_roots.insert(event.id); | ||
| 1993 | report.issues.insert(event.id); | ||
| 1994 | if !request.contributors.contains(&event.pubkey) | ||
| 1995 | && !fresh_profiles.contains(&event.pubkey) | ||
| 1996 | { | ||
| 1997 | fresh_profiles.insert(event.pubkey); | ||
| 1998 | } | ||
| 1979 | } else if [Kind::RelayList, Kind::Metadata, KIND_USER_GRASP_LIST].contains(&event.kind) | 1999 | } else if [Kind::RelayList, Kind::Metadata, KIND_USER_GRASP_LIST].contains(&event.kind) |
| 1980 | { | 2000 | { |
| 1981 | if request.missing_contributor_profiles.contains(&event.pubkey) { | 2001 | if request.missing_contributor_profiles.contains(&event.pubkey) { |
| @@ -2001,23 +2021,44 @@ async fn process_fetched_events( | |||
| 2001 | } | 2021 | } |
| 2002 | } | 2022 | } |
| 2003 | for event in &events { | 2023 | for event in &events { |
| 2004 | if !request.existing_events.contains(&event.id) | 2024 | if !request.existing_events.contains(&event.id) { |
| 2005 | && !event.tags.iter().any(|t| { | 2025 | let tagged_root_id = event.tags.iter().find_map(|t| { |
| 2006 | t.as_slice().len() > 1 | 2026 | if t.as_slice().len() > 1 |
| 2007 | && (t.as_slice()[0].eq("E") || t.as_slice()[0].eq("e")) | 2027 | && (t.as_slice()[0].eq("E") || t.as_slice()[0].eq("e")) |
| 2008 | && if let Ok(id) = EventId::parse(&t.as_slice()[1]) { | 2028 | { |
| 2009 | report.proposals.contains(&id) | 2029 | EventId::parse(&t.as_slice()[1]).ok() |
| 2030 | } else { | ||
| 2031 | None | ||
| 2032 | } | ||
| 2033 | }); | ||
| 2034 | if status_kinds().contains(&event.kind) { | ||
| 2035 | // Route status events to the correct counter based on whether | ||
| 2036 | // the root event is a known issue or a proposal (patch/PR). | ||
| 2037 | // Don't double-count statuses that arrived in the same batch | ||
| 2038 | // as their parent (new issues/proposals already inflate the count). | ||
| 2039 | if let Some(root_id) = &tagged_root_id { | ||
| 2040 | if report.issues.contains(root_id) { | ||
| 2041 | // status for a new issue in this batch — skip (counted via issues) | ||
| 2042 | } else if report.proposals.contains(root_id) { | ||
| 2043 | // status for a new proposal in this batch — skip (counted via proposals) | ||
| 2044 | } else if request.issue_ids.contains(root_id) { | ||
| 2045 | report.issue_statuses.insert(event.id); | ||
| 2010 | } else { | 2046 | } else { |
| 2011 | false | 2047 | report.statuses.insert(event.id); |
| 2012 | } | 2048 | } |
| 2013 | }) | 2049 | } |
| 2014 | { | 2050 | } else { |
| 2015 | if (event.kind.eq(&Kind::GitPatch) && !event_is_patch_set_root(event)) | 2051 | // Non-status events: commits/PR-updates for proposals only. |
| 2016 | || event.kind.eq(&KIND_PULL_REQUEST_UPDATE) | 2052 | let not_tagged_with_new_proposal = tagged_root_id |
| 2017 | { | 2053 | .as_ref() |
| 2018 | report.commits.insert(event.id); | 2054 | .is_none_or(|id| !report.proposals.contains(id)); |
| 2019 | } else if status_kinds().contains(&event.kind) { | 2055 | if not_tagged_with_new_proposal { |
| 2020 | report.statuses.insert(event.id); | 2056 | if (event.kind.eq(&Kind::GitPatch) && !event_is_patch_set_root(event)) |
| 2057 | || event.kind.eq(&KIND_PULL_REQUEST_UPDATE) | ||
| 2058 | { | ||
| 2059 | report.commits.insert(event.id); | ||
| 2060 | } | ||
| 2061 | } | ||
| 2021 | } | 2062 | } |
| 2022 | } | 2063 | } |
| 2023 | } | 2064 | } |
| @@ -2070,6 +2111,12 @@ pub fn consolidate_fetch_reports(reports: Vec<Result<FetchReport>>) -> FetchRepo | |||
| 2070 | for c in relay_report.statuses { | 2111 | for c in relay_report.statuses { |
| 2071 | report.statuses.insert(c); | 2112 | report.statuses.insert(c); |
| 2072 | } | 2113 | } |
| 2114 | for c in relay_report.issues { | ||
| 2115 | report.issues.insert(c); | ||
| 2116 | } | ||
| 2117 | for c in relay_report.issue_statuses { | ||
| 2118 | report.issue_statuses.insert(c); | ||
| 2119 | } | ||
| 2073 | report.deletions += relay_report.deletions; | 2120 | report.deletions += relay_report.deletions; |
| 2074 | for c in relay_report.contributor_profiles { | 2121 | for c in relay_report.contributor_profiles { |
| 2075 | report.contributor_profiles.insert(c); | 2122 | report.contributor_profiles.insert(c); |
| @@ -2107,6 +2154,7 @@ pub fn consolidate_fetch_reports(reports: Vec<Result<FetchReport>>) -> FetchRepo | |||
| 2107 | pub fn get_fetch_filters( | 2154 | pub fn get_fetch_filters( |
| 2108 | repo_coordinates: &HashSet<Nip19Coordinate>, | 2155 | repo_coordinates: &HashSet<Nip19Coordinate>, |
| 2109 | proposal_ids: &HashSet<EventId>, | 2156 | proposal_ids: &HashSet<EventId>, |
| 2157 | issue_ids: &HashSet<EventId>, | ||
| 2110 | non_proposal_event_ids: &HashSet<EventId>, | 2158 | non_proposal_event_ids: &HashSet<EventId>, |
| 2111 | required_profiles: &HashSet<PublicKey>, | 2159 | required_profiles: &HashSet<PublicKey>, |
| 2112 | ) -> Vec<nostr::Filter> { | 2160 | ) -> Vec<nostr::Filter> { |
| @@ -2118,7 +2166,12 @@ pub fn get_fetch_filters( | |||
| 2118 | get_filter_state_events(repo_coordinates, false), | 2166 | get_filter_state_events(repo_coordinates, false), |
| 2119 | get_filter_repo_ann_events(repo_coordinates, false), | 2167 | get_filter_repo_ann_events(repo_coordinates, false), |
| 2120 | nostr::Filter::default() | 2168 | nostr::Filter::default() |
| 2121 | .kinds(vec![Kind::GitPatch, Kind::EventDeletion, KIND_PULL_REQUEST]) | 2169 | .kinds(vec![ |
| 2170 | Kind::GitPatch, | ||
| 2171 | Kind::EventDeletion, | ||
| 2172 | KIND_PULL_REQUEST, | ||
| 2173 | Kind::GitIssue, | ||
| 2174 | ]) | ||
| 2122 | .custom_tags( | 2175 | .custom_tags( |
| 2123 | SingleLetterTag::lowercase(nostr_sdk::Alphabet::A), | 2176 | SingleLetterTag::lowercase(nostr_sdk::Alphabet::A), |
| 2124 | repo_coordinates | 2177 | repo_coordinates |
| @@ -2157,6 +2210,19 @@ pub fn get_fetch_filters( | |||
| 2157 | ), | 2210 | ), |
| 2158 | ] | 2211 | ] |
| 2159 | }, | 2212 | }, |
| 2213 | // Fetch status events for known issues. | ||
| 2214 | if issue_ids.is_empty() { | ||
| 2215 | vec![] | ||
| 2216 | } else { | ||
| 2217 | vec![ | ||
| 2218 | nostr::Filter::default() | ||
| 2219 | .events(issue_ids.clone()) | ||
| 2220 | .kinds(status_kinds()), | ||
| 2221 | nostr::Filter::default() | ||
| 2222 | .custom_tags(SingleLetterTag::uppercase(Alphabet::E), issue_ids.clone()) | ||
| 2223 | .kinds(status_kinds()), | ||
| 2224 | ] | ||
| 2225 | }, | ||
| 2160 | // Request kind-5 deletions for state events and repo announcements by | 2226 | // Request kind-5 deletions for state events and repo announcements by |
| 2161 | // their event ID (#e tag), as per NIP-09. The #a-tagged filter above | 2227 | // their event ID (#e tag), as per NIP-09. The #a-tagged filter above |
| 2162 | // covers addressable-event deletions; this covers the specific event IDs | 2228 | // covers addressable-event deletions; this covers the specific event IDs |
| @@ -2241,6 +2307,8 @@ pub struct FetchReport { | |||
| 2241 | /// commits against existing propoals | 2307 | /// commits against existing propoals |
| 2242 | commits: HashSet<EventId>, | 2308 | commits: HashSet<EventId>, |
| 2243 | statuses: HashSet<EventId>, | 2309 | statuses: HashSet<EventId>, |
| 2310 | issues: HashSet<EventId>, | ||
| 2311 | issue_statuses: HashSet<EventId>, | ||
| 2244 | /// Count of kind-5 deletion events received (for display purposes). | 2312 | /// Count of kind-5 deletion events received (for display purposes). |
| 2245 | deletions: u32, | 2313 | deletions: u32, |
| 2246 | contributor_profiles: HashSet<PublicKey>, | 2314 | contributor_profiles: HashSet<PublicKey>, |
| @@ -2304,6 +2372,20 @@ impl Display for FetchReport { | |||
| 2304 | if self.statuses.len() > 1 { "es" } else { "" }, | 2372 | if self.statuses.len() > 1 { "es" } else { "" }, |
| 2305 | )); | 2373 | )); |
| 2306 | } | 2374 | } |
| 2375 | if !self.issues.is_empty() { | ||
| 2376 | display_items.push(format!( | ||
| 2377 | "{} issue{}", | ||
| 2378 | self.issues.len(), | ||
| 2379 | if self.issues.len() > 1 { "s" } else { "" }, | ||
| 2380 | )); | ||
| 2381 | } | ||
| 2382 | if !self.issue_statuses.is_empty() { | ||
| 2383 | display_items.push(format!( | ||
| 2384 | "{} issue status{}", | ||
| 2385 | self.issue_statuses.len(), | ||
| 2386 | if self.issue_statuses.len() > 1 { "es" } else { "" }, | ||
| 2387 | )); | ||
| 2388 | } | ||
| 2307 | if self.deletions > 0 { | 2389 | if self.deletions > 0 { |
| 2308 | display_items.push(format!( | 2390 | display_items.push(format!( |
| 2309 | "{} deletion{}", | 2391 | "{} deletion{}", |
| @@ -2345,6 +2427,8 @@ pub struct FetchRequest { | |||
| 2345 | repo_coordinates_without_relays: Vec<(Nip19Coordinate, Option<Timestamp>)>, | 2427 | repo_coordinates_without_relays: Vec<(Nip19Coordinate, Option<Timestamp>)>, |
| 2346 | state: Option<(Timestamp, EventId)>, | 2428 | state: Option<(Timestamp, EventId)>, |
| 2347 | proposals: HashSet<EventId>, | 2429 | proposals: HashSet<EventId>, |
| 2430 | /// Known issue event IDs, used to fetch their status events. | ||
| 2431 | issue_ids: HashSet<EventId>, | ||
| 2348 | /// Event IDs of non-proposal events (state events, repo announcements) for | 2432 | /// Event IDs of non-proposal events (state events, repo announcements) for |
| 2349 | /// which we should also request kind-5 deletion events by `#e` tag. | 2433 | /// which we should also request kind-5 deletion events by `#e` tag. |
| 2350 | non_proposal_event_ids: HashSet<EventId>, | 2434 | non_proposal_event_ids: HashSet<EventId>, |