Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

database: add support to event deletion by coordinates #200

Merged
merged 10 commits into from
Dec 4, 2023
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ jobs:
-p nostr --no-default-features --features alloc,
-p nostr --no-default-features --features "alloc all-nips",
-p nostr --features blocking,
-p nostr-database
-p nostr-database,
-p nostr-sdk,
-p nostr-sdk --no-default-features,
-p nostr-sdk --features blocking,
Expand Down
147 changes: 134 additions & 13 deletions crates/nostr-database/src/index.rs
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,7 @@ impl DatabaseIndexes {

if event.is_replaceable() {
let filter: Filter = Filter::new().author(event.pubkey).kind(event.kind);
for ev in self.internal_query(&index, filter).await {
for ev in self.internal_query(&index, filter, false).await {
if ev.created_at > event.created_at {
should_insert = false;
} else if ev.created_at <= event.created_at {
Expand All @@ -179,7 +179,7 @@ impl DatabaseIndexes {
.author(event.pubkey)
.kind(event.kind)
.identifier(identifier);
for ev in self.internal_query(&index, filter).await {
for ev in self.internal_query(&index, filter, false).await {
if ev.created_at >= event.created_at {
should_insert = false;
} else if ev.created_at < event.created_at {
Expand All @@ -191,16 +191,30 @@ impl DatabaseIndexes {
}
} else if event.kind == Kind::EventDeletion {
let mut deleted = self.deleted.write().await;
let ids = event.event_ids().copied();
let filter = Filter::new().ids(ids);
let pubkey_prefix: PublicKeyPrefix = PublicKeyPrefix::from(event.pubkey);
for ev in self.internal_query(&index, filter).await {

// Check `e` tags
let ids = event.event_ids().copied();
let filter: Filter = Filter::new().ids(ids);
for ev in self.internal_query(&index, filter, false).await {
if ev.pubkey == pubkey_prefix {
to_discard.insert(ev.event_id);
deleted.insert(ev.event_id);
}
}
// TODO: support event deletion by coordinate (`a` tag)

// Check `a` tags
for coordinate in event.coordinates() {
let coordinate_pubkey_prefix: PublicKeyPrefix =
PublicKeyPrefix::from(coordinate.pubkey);
if coordinate_pubkey_prefix == pubkey_prefix {
let filter: Filter = coordinate.into();
for ev in self.internal_query(&index, filter, false).await {
to_discard.insert(ev.event_id);
deleted.insert(ev.event_id);
}
}
}
}

// Remove events
Expand All @@ -223,19 +237,24 @@ impl DatabaseIndexes {
&self,
index: &'a BTreeSet<EventIndex>,
filter: Filter,
allow_empty_filter: bool,
) -> impl Iterator<Item = &'a EventIndex> {
let authors: HashSet<PublicKeyPrefix> = filter
.authors
.iter()
.map(|p| PublicKeyPrefix::from(*p))
.collect();
index.iter().filter(move |m| {
(filter.ids.is_empty() || filter.ids.contains(&m.event_id))
&& filter.since.map_or(true, |t| m.created_at >= t)
&& filter.until.map_or(true, |t| m.created_at <= t)
&& (filter.authors.is_empty() || authors.contains(&m.pubkey))
&& (filter.kinds.is_empty() || filter.kinds.contains(&m.kind))
&& m.filter_tags_match(&filter)
if (filter.is_empty() && allow_empty_filter) || !filter.is_empty() {
(filter.ids.is_empty() || filter.ids.contains(&m.event_id))
&& filter.since.map_or(true, |t| m.created_at >= t)
&& filter.until.map_or(true, |t| m.created_at <= t)
&& (filter.authors.is_empty() || authors.contains(&m.pubkey))
&& (filter.kinds.is_empty() || filter.kinds.contains(&m.kind))
&& m.filter_tags_match(&filter)
} else {
false
}
})
}

Expand All @@ -257,7 +276,7 @@ impl DatabaseIndexes {
}

let limit: Option<usize> = filter.limit;
let iter = self.internal_query(&index, filter).await;
let iter = self.internal_query(&index, filter, true).await;
if let Some(limit) = limit {
matching_ids.extend(iter.take(limit))
} else {
Expand All @@ -280,3 +299,105 @@ impl DatabaseIndexes {
index.clear();
}
}

#[cfg(test)]
mod tests {
use nostr::nips::nip01::Coordinate;
use nostr::secp256k1::SecretKey;
use nostr::{EventBuilder, FromBech32, Keys, Tag};

use super::*;

const SECRET_KEY_A: &str = "nsec1ufnus6pju578ste3v90xd5m2decpuzpql2295m3sknqcjzyys9ls0qlc85";
const SECRET_KEY_B: &str = "nsec1j4c6269y9w0q2er2xjw8sv2ehyrtfxq3jwgdlxj6qfn8z4gjsq5qfvfk99";

#[tokio::test]
async fn test_event_deletion() {
let indexes = DatabaseIndexes::new();

// Keys
let keys_a = Keys::new(SecretKey::from_bech32(SECRET_KEY_A).unwrap());
let keys_b = Keys::new(SecretKey::from_bech32(SECRET_KEY_B).unwrap());

// Build some events
let events = [
EventBuilder::new_text_note("Text note", &[])
.to_event(&keys_a)
.unwrap(),
EventBuilder::new(
Kind::ParameterizedReplaceable(32121),
"Empty 1",
&[Tag::Identifier(String::from("abdefgh:12345678"))],
)
.to_event(&keys_a)
.unwrap(),
EventBuilder::new(
Kind::ParameterizedReplaceable(32122),
"Empty 2",
&[Tag::Identifier(String::from("abdefgh:12345678"))],
)
.to_event(&keys_a)
.unwrap(),
EventBuilder::new(
Kind::ParameterizedReplaceable(32122),
"",
&[Tag::Identifier(String::from("ijklmnop:87654321"))],
)
.to_event(&keys_a)
.unwrap(),
EventBuilder::new(
Kind::ParameterizedReplaceable(32122),
"",
&[Tag::Identifier(String::from("abdefgh:87654321"))],
)
.to_event(&keys_b)
.unwrap(),
EventBuilder::new(
Kind::ParameterizedReplaceable(32122),
"",
&[Tag::Identifier(String::from("abdefgh:12345678"))],
)
.to_event(&keys_b)
.unwrap(),
EventBuilder::new(
Kind::ParameterizedReplaceable(32122),
"Test",
&[Tag::Identifier(String::from("abdefgh:12345678"))],
)
.to_event(&keys_a)
.unwrap(),
];

for event in events.iter() {
indexes.index_event(event).await;
}

// Total events: 6

// Invalid event deletion (wrong signing keys)
let coordinate =
Coordinate::new(Kind::ParameterizedReplaceable(32122), keys_a.public_key());
let event = EventBuilder::delete([coordinate])
.to_event(&keys_b)
.unwrap();
indexes.index_event(&event).await;

// Total events: 7 (6 + 1)

// Delete valid event
let coordinate =
Coordinate::new(Kind::ParameterizedReplaceable(32122), keys_a.public_key())
.identifier("ijklmnop:87654321");
let event = EventBuilder::delete([coordinate])
.to_event(&keys_a)
.unwrap();
indexes.index_event(&event).await;

// Total events: 7 (7 - 1 + 1)

// Check total number of indexes
let filter = Filter::new();
let res = indexes.query([filter]).await;
assert_eq!(res.len(), 7);
}
}
1 change: 1 addition & 0 deletions crates/nostr-sdk/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@ The following crate feature flags are available:
| Feature | Default | Description |
| ------------------- | :-----: | ---------------------------------------------------------------------------------------- |
| `blocking` | No | Needed to use `NIP-05` and `NIP-11` features in not async/await context |
| `sqlite` | No | Enable SQLite Storage backend |
| `rocksdb` | No | Enable RocksDB Storage backend |
| `indexeddb` | No | Enable Web's IndexedDb Storage backend |
| `all-nips` | Yes | Enable all NIPs |
Expand Down
1 change: 0 additions & 1 deletion crates/nostr/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,6 @@ The following crate feature flags are available:
| ✅ | [30 - Custom Emoji](https://github.com/nostr-protocol/nips/blob/master/30.md) |
| ❌ | [31 - Dealing with Unknown Events](https://github.com/nostr-protocol/nips/blob/master/31.md) |
| ❌ | [32 - Labeling](https://github.com/nostr-protocol/nips/blob/master/32.md) |
| ✅ | [33 - Parameterized Replaceable Events](https://github.com/nostr-protocol/nips/blob/master/33.md) |
| ✅ | [36 - Sensitive Content](https://github.com/nostr-protocol/nips/blob/master/36.md) |
| ✅ | [39 - External Identities in Profiles](https://github.com/nostr-protocol/nips/blob/master/39.md) |
| ✅ | [40 - Expiration Timestamp](https://github.com/nostr-protocol/nips/blob/master/40.md) |
Expand Down
16 changes: 11 additions & 5 deletions crates/nostr/src/event/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ use crate::nips::{nip13, nip58};
use crate::types::time::Instant;
use crate::types::time::TimeSupplier;
use crate::types::{ChannelId, Contact, Metadata, Timestamp};
use crate::util::EventIdOrCoordinate;
#[cfg(feature = "std")]
use crate::SECP256K1;
use crate::{JsonUtil, RelayMetadata, UncheckedUrl};
Expand Down Expand Up @@ -440,22 +441,27 @@ impl EventBuilder {
}

/// Create delete event
pub fn delete<I>(ids: I) -> Self
pub fn delete<I, T>(ids: I) -> Self
where
I: IntoIterator<Item = EventId>,
I: IntoIterator<Item = T>,
T: Into<EventIdOrCoordinate>,
{
Self::delete_with_reason(ids, "")
}

/// Create delete event with reason
pub fn delete_with_reason<I, S>(ids: I, reason: S) -> Self
pub fn delete_with_reason<I, T, S>(ids: I, reason: S) -> Self
where
I: IntoIterator<Item = EventId>,
I: IntoIterator<Item = T>,
T: Into<EventIdOrCoordinate>,
S: Into<String>,
{
let tags: Vec<Tag> = ids
.into_iter()
.map(|id| Tag::Event(id, None, None))
.map(|t| {
let middle: EventIdOrCoordinate = t.into();
middle.into()
})
.collect();

Self::new(Kind::EventDeletion, reason.into(), &tags)
Expand Down
6 changes: 6 additions & 0 deletions crates/nostr/src/event/id.rs
Original file line number Diff line number Diff line change
Expand Up @@ -150,3 +150,9 @@ impl From<EventId> for String {
event_id.to_string()
}
}

impl From<EventId> for Tag {
fn from(id: EventId) -> Self {
Tag::Event(id, None, None)
}
}
6 changes: 6 additions & 0 deletions crates/nostr/src/event/kind.rs
Original file line number Diff line number Diff line change
Expand Up @@ -386,4 +386,10 @@ mod tests {
assert_ne!(Kind::Authentication, Kind::EncryptedDirectMessage);
assert_ne!(Kind::TextNote, Kind::Custom(2));
}

#[test]
fn test_kind_is_parameterized_replaceable() {
assert!(Kind::ParameterizedReplaceable(32122).is_parameterized_replaceable());
assert!(!Kind::ParameterizedReplaceable(1).is_parameterized_replaceable());
}
}
19 changes: 19 additions & 0 deletions crates/nostr/src/event/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ pub use self::kind::Kind;
pub use self::partial::{MissingPartialEvent, PartialEvent};
pub use self::tag::{Marker, Tag, TagIndexValues, TagIndexes, TagKind};
pub use self::unsigned::UnsignedEvent;
use crate::nips::nip01::Coordinate;
#[cfg(feature = "std")]
use crate::types::time::Instant;
use crate::types::time::TimeSupplier;
Expand Down Expand Up @@ -292,6 +293,24 @@ impl Event {
})
}

/// Extract coordinates from tags (`a` tag)
pub fn coordinates(&self) -> impl Iterator<Item = Coordinate> + '_ {
self.tags.iter().filter_map(|t| match t {
Tag::A {
kind,
public_key,
identifier,
..
} => Some(Coordinate {
kind: *kind,
pubkey: *public_key,
identifier: identifier.clone(),
relays: Vec::new(),
}),
_ => None,
})
}

/// Build tags index
pub fn build_tags_index(&self) -> TagIndexes {
TagIndexes::from(self.tags.iter().map(|t| t.as_vec()))
Expand Down
14 changes: 14 additions & 0 deletions crates/nostr/src/message/subscription.rs
Original file line number Diff line number Diff line change
Expand Up @@ -683,6 +683,11 @@ impl Filter {
&& self.authors_match(event)
&& self.tag_match(event)
}

/// Check if [`Filter`] is empty
pub fn is_empty(&self) -> bool {
self == &Filter::default()
}
}

/// Filters match event trait
Expand Down Expand Up @@ -980,4 +985,13 @@ mod test {
assert!(!filter.match_event(&event));
assert!(!filter.match_event(&event_with_empty_tags));
}

#[test]
fn test_filter_is_empty() {
let filter = Filter::new().identifier("test");
assert!(!filter.is_empty());

let filter = Filter::new();
assert!(filter.is_empty());
}
}
2 changes: 2 additions & 0 deletions crates/nostr/src/nips/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
//!
//! See all at <https://github.com/nostr-protocol/nips>

pub mod nip01;
#[cfg(feature = "nip04")]
pub mod nip04;
#[cfg(all(feature = "std", feature = "nip05"))]
Expand All @@ -18,6 +19,7 @@ pub mod nip15;
pub mod nip19;
pub mod nip21;
pub mod nip26;
#[deprecated(since = "0.26.0", note = "moved to `nip01`")]
pub mod nip33;
#[cfg(feature = "nip44")]
pub mod nip44;
Expand Down
Loading