add support deletion
This commit is contained in:
@@ -0,0 +1,79 @@
|
|||||||
|
use std::collections::HashSet;
|
||||||
|
|
||||||
|
use nostr::prelude::*;
|
||||||
|
|
||||||
|
/// NIP-09 deletion requests and NIP-62 vanish requests, used to hide
|
||||||
|
/// deleted events before they reach the UI.
|
||||||
|
///
|
||||||
|
/// Built from the kind-5 / kind-62 events stored in the local database;
|
||||||
|
/// pass any event through [`Deletions::is_deleted`] before displaying it.
|
||||||
|
pub struct Deletions {
|
||||||
|
/// `(deleted event id, expected author)` from `e` tags of kind-5 events.
|
||||||
|
ids: HashSet<(EventId, PublicKey)>,
|
||||||
|
/// `(coordinate, expected author, cutoff)` from `a` tags of kind-5 events.
|
||||||
|
/// All versions of the addressable event up to `cutoff` are deleted.
|
||||||
|
coords: Vec<(Coordinate, PublicKey, Timestamp)>,
|
||||||
|
/// `(author, cutoff)` from kind-62 vanish requests.
|
||||||
|
vanished: Vec<(PublicKey, Timestamp)>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Deletions {
|
||||||
|
/// Build the deletion index from raw kind-5 and kind-62 events.
|
||||||
|
pub fn from_events(events: impl IntoIterator<Item = Event>) -> Self {
|
||||||
|
let mut ids = HashSet::new();
|
||||||
|
let mut coords = Vec::new();
|
||||||
|
let mut vanished = Vec::new();
|
||||||
|
|
||||||
|
for event in events {
|
||||||
|
if event.kind == Kind::EventDeletion {
|
||||||
|
ids.extend(event.tags.event_ids().map(|id| (id, event.pubkey)));
|
||||||
|
coords.extend(
|
||||||
|
event
|
||||||
|
.tags
|
||||||
|
.coordinates()
|
||||||
|
.map(|c| (c, event.pubkey, event.created_at)),
|
||||||
|
);
|
||||||
|
} else if event.kind == Kind::RequestToVanish {
|
||||||
|
// Client-side we can't verify which relay the request targeted,
|
||||||
|
// so any vanish request is honored for the author's events.
|
||||||
|
vanished.push((event.pubkey, event.created_at));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Self {
|
||||||
|
ids,
|
||||||
|
coords,
|
||||||
|
vanished,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether the event is covered by a valid deletion or vanish request.
|
||||||
|
///
|
||||||
|
/// A request is only valid when its author matches the deleted event's
|
||||||
|
/// author (NIP-09); addressable events are deleted up to the request's
|
||||||
|
/// `created_at`.
|
||||||
|
pub fn is_deleted(&self, event: &Event) -> bool {
|
||||||
|
if self
|
||||||
|
.vanished
|
||||||
|
.iter()
|
||||||
|
.any(|(pk, cutoff)| *pk == event.pubkey && event.created_at <= *cutoff)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.ids.contains(&(event.id, event.pubkey)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if event.kind.is_addressable()
|
||||||
|
&& let Some(identifier) = event.tags.identifier()
|
||||||
|
{
|
||||||
|
let coordinate = Coordinate::new(event.kind, event.pubkey).identifier(identifier);
|
||||||
|
return self.coords.iter().any(|(c, pk, cutoff)| {
|
||||||
|
*c == coordinate && *pk == event.pubkey && event.created_at <= *cutoff
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -72,3 +72,23 @@ pub fn announcements_by(public_key: PublicKey) -> Filter {
|
|||||||
pub fn all_announcements() -> Filter {
|
pub fn all_announcements() -> Filter {
|
||||||
Filter::new().kind(Kind::GitRepoAnnouncement)
|
Filter::new().kind(Kind::GitRepoAnnouncement)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// All deletion-related events (NIP-09 kind `5`, NIP-62 kind `62`).
|
||||||
|
///
|
||||||
|
/// Unbounded, like [`all_announcements`]: deletion requests must be known
|
||||||
|
/// before any other event can be shown.
|
||||||
|
pub fn deletions() -> Filter {
|
||||||
|
Filter::new().kinds([Kind::EventDeletion, Kind::RequestToVanish])
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Deletion events relevant to a single repository: requests authored by
|
||||||
|
/// the repository owner and requests addressed to the repository
|
||||||
|
/// coordinate (`#a` tag).
|
||||||
|
pub fn deletions_for_repo(addr: &RepoAddr) -> Vec<Filter> {
|
||||||
|
vec![
|
||||||
|
Filter::new()
|
||||||
|
.kinds([Kind::EventDeletion, Kind::RequestToVanish])
|
||||||
|
.author(addr.public_key),
|
||||||
|
Filter::new().kind(Kind::EventDeletion).coordinate(addr),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
pub mod addr;
|
pub mod addr;
|
||||||
pub mod clone_url;
|
pub mod clone_url;
|
||||||
|
pub mod deletions;
|
||||||
pub mod filters;
|
pub mod filters;
|
||||||
pub mod model;
|
pub mod model;
|
||||||
pub mod state;
|
pub mod state;
|
||||||
@@ -7,6 +8,7 @@ pub mod status;
|
|||||||
|
|
||||||
pub use addr::{RepoAddr, repo_addr};
|
pub use addr::{RepoAddr, repo_addr};
|
||||||
pub use clone_url::{CloneTarget, parse_clone_url};
|
pub use clone_url::{CloneTarget, parse_clone_url};
|
||||||
|
pub use deletions::Deletions;
|
||||||
pub use model::Announcement;
|
pub use model::Announcement;
|
||||||
pub use state::parse_state;
|
pub use state::parse_state;
|
||||||
pub use status::{RepoStatus, references_root, resolve_status};
|
pub use status::{RepoStatus, references_root, resolve_status};
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ use std::time::Duration;
|
|||||||
use anyhow::Error;
|
use anyhow::Error;
|
||||||
use gpui::{AppContext, Context, Subscription, Task};
|
use gpui::{AppContext, Context, Subscription, Task};
|
||||||
use nostr_sdk::prelude::*;
|
use nostr_sdk::prelude::*;
|
||||||
use signed_core::{Announcement, RepoAddr, RepoStatus, filters, parse_state};
|
use signed_core::{Announcement, Deletions, RepoAddr, RepoStatus, filters, parse_state};
|
||||||
|
|
||||||
use crate::backend::{Backend, BackendEvent};
|
use crate::backend::{Backend, BackendEvent};
|
||||||
|
|
||||||
@@ -41,11 +41,14 @@ impl RepoStore {
|
|||||||
let subscription = cx.subscribe(&backend, |this, _backend, event, cx| {
|
let subscription = cx.subscribe(&backend, |this, _backend, event, cx| {
|
||||||
let relevant = match event {
|
let relevant = match event {
|
||||||
BackendEvent::NostrUpdate(update) => {
|
BackendEvent::NostrUpdate(update) => {
|
||||||
|
// Deletions may target any event of this repository.
|
||||||
|
let deletion =
|
||||||
|
update.kind == Kind::EventDeletion || update.kind == Kind::RequestToVanish;
|
||||||
let coordinate = update.coordinate.as_ref() == Some(&this.addr);
|
let coordinate = update.coordinate.as_ref() == Some(&this.addr);
|
||||||
let author = update.author == this.addr.public_key;
|
let author = update.author == this.addr.public_key;
|
||||||
let kind = update.kind == Kind::GitRepoAnnouncement;
|
let kind = update.kind == Kind::GitRepoAnnouncement;
|
||||||
|
|
||||||
coordinate || (author && kind)
|
deletion || coordinate || (author && kind)
|
||||||
}
|
}
|
||||||
BackendEvent::Published(event) => {
|
BackendEvent::Published(event) => {
|
||||||
let kind = event.kind == Kind::GitRepoAnnouncement;
|
let kind = event.kind == Kind::GitRepoAnnouncement;
|
||||||
@@ -94,14 +97,15 @@ impl RepoStore {
|
|||||||
let addr = self.addr.clone();
|
let addr = self.addr.clone();
|
||||||
|
|
||||||
Backend::global(cx).update(cx, |backend, cx| {
|
Backend::global(cx).update(cx, |backend, cx| {
|
||||||
backend.subscribe_bootstrap(
|
let mut repo_filters = vec![
|
||||||
vec![
|
filters::announcement(&addr),
|
||||||
filters::announcement(&addr),
|
filters::state(&addr),
|
||||||
filters::state(&addr),
|
filters::activity(&addr),
|
||||||
filters::activity(&addr),
|
];
|
||||||
],
|
// Deletion requests (NIP-09/62) must be known before any
|
||||||
cx,
|
// event of this repository can be shown.
|
||||||
);
|
repo_filters.extend(filters::deletions_for_repo(&addr));
|
||||||
|
backend.subscribe_bootstrap(repo_filters, cx);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -141,31 +145,38 @@ impl RepoStore {
|
|||||||
let addr = self.addr.clone();
|
let addr = self.addr.clone();
|
||||||
|
|
||||||
let work = cx.background_spawn(async move {
|
let work = cx.background_spawn(async move {
|
||||||
let queries = async {
|
let (announcements, states, activity, deletion_events) = async {
|
||||||
let db = client.database();
|
let db = client.database();
|
||||||
|
|
||||||
let announcements = db.query(filters::announcement(&addr)).await?;
|
let announcements = db.query(filters::announcement(&addr)).await?;
|
||||||
let states = db.query(filters::state(&addr)).await?;
|
let states = db.query(filters::state(&addr)).await?;
|
||||||
let activity = db.query(filters::activity(&addr)).await?;
|
let activity = db.query(filters::activity(&addr)).await?;
|
||||||
|
let deletion_events = db.query(filters::deletions()).await?;
|
||||||
|
|
||||||
Ok::<_, Error>((announcements, states, activity))
|
Ok::<_, Error>((announcements, states, activity, deletion_events))
|
||||||
}
|
}
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let (announcements, states, activity) = queries;
|
let deletions = Deletions::from_events(deletion_events);
|
||||||
|
|
||||||
// Parse and sort off the main thread; only plain data
|
// Parse and sort off the main thread; only plain data
|
||||||
// crosses back into the entity.
|
// crosses back into the entity.
|
||||||
let announcement = latest(announcements)
|
let all_announcements = announcements
|
||||||
|
.into_iter()
|
||||||
|
.filter(|e| !deletions.is_deleted(e));
|
||||||
|
let announcement = latest(all_announcements)
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.and_then(Announcement::from_event);
|
.and_then(Announcement::from_event);
|
||||||
|
|
||||||
let state = latest(states).map(|state| parse_state(&state));
|
let all_states = states.into_iter().filter(|e| !deletions.is_deleted(e));
|
||||||
|
let state = latest(all_states).map(|state| parse_state(&state));
|
||||||
|
|
||||||
let (mut issues, mut patches, mut pull_requests, mut statuses) =
|
let (mut issues, mut patches, mut pull_requests, mut statuses) =
|
||||||
(Vec::new(), Vec::new(), Vec::new(), Vec::new());
|
(Vec::new(), Vec::new(), Vec::new(), Vec::new());
|
||||||
|
|
||||||
for event in activity {
|
for event in activity {
|
||||||
|
if deletions.is_deleted(&event) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
match event.kind {
|
match event.kind {
|
||||||
Kind::GitIssue => issues.push(event),
|
Kind::GitIssue => issues.push(event),
|
||||||
Kind::GitPatch => patches.push(event),
|
Kind::GitPatch => patches.push(event),
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ use std::time::Duration;
|
|||||||
use anyhow::Error;
|
use anyhow::Error;
|
||||||
use gpui::{AppContext, Context, Subscription, Task};
|
use gpui::{AppContext, Context, Subscription, Task};
|
||||||
use nostr_sdk::prelude::*;
|
use nostr_sdk::prelude::*;
|
||||||
use signed_core::{Announcement, RepoAddr, filters, repo_addr};
|
use signed_core::{Announcement, Deletions, RepoAddr, filters, repo_addr};
|
||||||
|
|
||||||
use crate::backend::{Backend, BackendEvent};
|
use crate::backend::{Backend, BackendEvent};
|
||||||
|
|
||||||
@@ -40,9 +40,12 @@ impl RepoListStore {
|
|||||||
let subscription = cx.subscribe(&backend, |this, _backend, event, cx| {
|
let subscription = cx.subscribe(&backend, |this, _backend, event, cx| {
|
||||||
let relevant = match event {
|
let relevant = match event {
|
||||||
BackendEvent::NostrUpdate(update) => {
|
BackendEvent::NostrUpdate(update) => {
|
||||||
// Activity (patches, issues, ...) is addressed to repos via
|
// Deletions may target anything we list; always refresh.
|
||||||
// `a` tags, so its author isn't the repo owner; always refresh.
|
if update.kind == Kind::EventDeletion || update.kind == Kind::RequestToVanish {
|
||||||
if filters::ACTIVITY_KINDS.contains(&update.kind) {
|
true
|
||||||
|
} else if filters::ACTIVITY_KINDS.contains(&update.kind) {
|
||||||
|
// Activity (patches, issues, ...) is addressed to repos via
|
||||||
|
// `a` tags, so its author isn't the repo owner; always refresh.
|
||||||
true
|
true
|
||||||
} else {
|
} else {
|
||||||
let is_announcement = update.kind == Kind::GitRepoAnnouncement;
|
let is_announcement = update.kind == Kind::GitRepoAnnouncement;
|
||||||
@@ -98,6 +101,9 @@ impl RepoListStore {
|
|||||||
None => filters::all_announcements(),
|
None => filters::all_announcements(),
|
||||||
};
|
};
|
||||||
backend.sync_bootstrap(filter, cx);
|
backend.sync_bootstrap(filter, cx);
|
||||||
|
// Deletion requests (NIP-09/62) must be known before any
|
||||||
|
// announcement can be shown.
|
||||||
|
backend.sync_bootstrap(filters::deletions(), cx);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -143,12 +149,18 @@ impl RepoListStore {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let events = client.database().query(filter).await?;
|
let events = client.database().query(filter).await?;
|
||||||
|
let deletion_events = client.database().query(filters::deletions()).await?;
|
||||||
|
let deletions = Deletions::from_events(deletion_events);
|
||||||
|
|
||||||
// Dedup and sort off the main thread; only the final list
|
// Dedup and sort off the main thread; only the final list
|
||||||
// crosses back into the entity.
|
// crosses back into the entity.
|
||||||
let mut by_repo: HashMap<RepoAddr, Announcement> = HashMap::new();
|
let mut by_repo: HashMap<RepoAddr, Announcement> = HashMap::new();
|
||||||
|
|
||||||
for event in events {
|
for event in events {
|
||||||
|
if deletions.is_deleted(&event) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
let Some(announcement) = Announcement::from_event(&event) else {
|
let Some(announcement) = Announcement::from_event(&event) else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
@@ -175,6 +187,9 @@ impl RepoListStore {
|
|||||||
|
|
||||||
let state_filter = Filter::new().kind(Kind::RepoState);
|
let state_filter = Filter::new().kind(Kind::RepoState);
|
||||||
for event in client.database().query(state_filter).await? {
|
for event in client.database().query(state_filter).await? {
|
||||||
|
if deletions.is_deleted(&event) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
let Some(id) = event.tags.identifier() else {
|
let Some(id) = event.tags.identifier() else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
@@ -191,16 +206,10 @@ impl RepoListStore {
|
|||||||
.kinds(filters::ACTIVITY_KINDS)
|
.kinds(filters::ACTIVITY_KINDS)
|
||||||
.since(Timestamp::now() - ACTIVITY_WINDOW);
|
.since(Timestamp::now() - ACTIVITY_WINDOW);
|
||||||
for event in client.database().query(activity_filter).await? {
|
for event in client.database().query(activity_filter).await? {
|
||||||
for tag in event.tags.iter() {
|
if deletions.is_deleted(&event) {
|
||||||
if tag.kind() != "a" {
|
continue;
|
||||||
continue;
|
}
|
||||||
}
|
for addr in event.tags.coordinates() {
|
||||||
let Some(content) = tag.content() else {
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
let Ok(addr) = Coordinate::parse(content) else {
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
if addr.kind != Kind::GitRepoAnnouncement {
|
if addr.kind != Kind::GitRepoAnnouncement {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user