commit 6673fcfd1177ce19c86f3a0b056629fe11fce81b
parent b5da3fa2b024ac957536bb8af5497400bb61ed8a
Author: Greg Heartsfield <scsibug@imap.cc>
Date: Sat, 1 Jan 2022 18:38:52 -0600
feat: implement multi-valued filter searching
NIP-01 now uses arrays instead of scalars.
Fixes https://todo.sr.ht/~gheartsfield/nostr-rs-relay/17
Diffstat:
M | src/db.rs | | | 59 | ++++++++++++++++++++++++++++++++++++++--------------------- |
M | src/subscription.rs | | | 60 | ++++++++++++++++++++++++++++++++++++++++++------------------ |
2 files changed, 80 insertions(+), 39 deletions(-)
diff --git a/src/db.rs b/src/db.rs
@@ -302,35 +302,52 @@ fn query_from_sub(sub: &Subscription) -> String {
filter_components.push(authors_clause);
}
// Query for Kind
- if f.kind.is_some() {
+ if let Some(ks) = &f.kinds {
// kind is number, no escaping needed
- let kind_clause = format!("kind = {}", f.kind.unwrap());
+ let str_kinds: Vec<String> = ks.iter().map(|x| x.to_string()).collect();
+ let kind_clause = format!("kind IN ({})", str_kinds.join(", "));
filter_components.push(kind_clause);
}
// Query for event
- if f.id.is_some() {
- let id_str = f.id.as_ref().unwrap();
- if is_hex(id_str) {
- let id_clause = format!("event_hash = x'{}'", id_str);
- filter_components.push(id_clause);
- }
+ if f.ids.is_some() {
+ let ids_escaped: Vec<String> = f
+ .ids
+ .as_ref()
+ .unwrap()
+ .iter()
+ .filter(|&x| is_hex(x))
+ .map(|x| format!("x'{}'", x))
+ .collect();
+ let id_clause = format!("event_hash IN ({})", ids_escaped.join(", "));
+ filter_components.push(id_clause);
}
// Query for referenced event
- if f.event.is_some() {
- let ev_str = f.event.as_ref().unwrap();
- if is_hex(ev_str) {
- let ev_clause = format!("referenced_event = x'{}'", ev_str);
- filter_components.push(ev_clause);
- }
+ if f.events.is_some() {
+ let events_escaped: Vec<String> = f
+ .events
+ .as_ref()
+ .unwrap()
+ .iter()
+ .filter(|&x| is_hex(x))
+ .map(|x| format!("x'{}'", x))
+ .collect();
+ let events_clause = format!("referenced_event IN ({})", events_escaped.join(", "));
+ filter_components.push(events_clause);
}
- // Query for referenced pet name pubkey
- if f.pubkey.is_some() {
- let pet_str = f.pubkey.as_ref().unwrap();
- if is_hex(pet_str) {
- let pet_clause = format!("referenced_pubkey = x'{}'", pet_str);
- filter_components.push(pet_clause);
- }
+ // Query for referenced pubkey
+ if f.pubkeys.is_some() {
+ let pubkeys_escaped: Vec<String> = f
+ .pubkeys
+ .as_ref()
+ .unwrap()
+ .iter()
+ .filter(|&x| is_hex(x))
+ .map(|x| format!("x'{}'", x))
+ .collect();
+ let pubkeys_clause = format!("referenced_pubkey IN ({})", pubkeys_escaped.join(", "));
+ filter_components.push(pubkeys_clause);
}
+
// Query for timestamp
if f.since.is_some() {
let created_clause = format!("created_at > {}", f.since.unwrap());
diff --git a/src/subscription.rs b/src/subscription.rs
@@ -2,6 +2,7 @@
use crate::error::Result;
use crate::event::Event;
use serde::{Deserialize, Deserializer, Serialize};
+use std::collections::HashSet;
/// Subscription identifier and set of request filters
#[derive(Serialize, PartialEq, Debug, Clone)]
@@ -17,16 +18,16 @@ pub struct Subscription {
/// absent ([`None`]) if it should be ignored.
#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
pub struct ReqFilter {
- /// Event hash
- pub id: Option<String>,
- /// Event kind
- pub kind: Option<u64>,
+ /// Event hashes
+ pub ids: Option<Vec<String>>,
+ /// Event kinds
+ pub kinds: Option<Vec<u64>>,
/// Referenced event hash
#[serde(rename = "#e")]
- pub event: Option<String>,
+ pub events: Option<Vec<String>>,
/// Referenced public key for a petname
#[serde(rename = "#p")]
- pub pubkey: Option<String>,
+ pub pubkeys: Option<Vec<String>>,
/// Events published after this time
pub since: Option<u64>,
/// Events published before this time
@@ -105,8 +106,13 @@ impl Subscription {
impl ReqFilter {
/// Check for a match within the authors list.
- // TODO: Ambiguity; what if the array is empty? Should we
- // consider that the same as null?
+ fn ids_match(&self, event: &Event) -> bool {
+ self.ids
+ .as_ref()
+ .map(|vs| vs.contains(&event.id.to_owned()))
+ .unwrap_or(true)
+ }
+
fn authors_match(&self, event: &Event) -> bool {
self.authors
.as_ref()
@@ -115,29 +121,47 @@ impl ReqFilter {
}
/// Check if this filter either matches, or does not care about the event tags.
fn event_match(&self, event: &Event) -> bool {
- self.event
- .as_ref()
- .map(|t| event.event_tag_match(t))
- .unwrap_or(true)
+ // This needs to be analyzed for performance; building these
+ // hash sets for each active subscription isn't great.
+ if let Some(es) = &self.events {
+ let event_refs =
+ HashSet::<_>::from_iter(event.get_event_tags().iter().map(|x| x.to_owned()));
+ let filter_refs = HashSet::<_>::from_iter(es.iter().map(|x| &x[..]));
+ let cardinality = event_refs.intersection(&filter_refs).count();
+ cardinality > 0
+ } else {
+ true
+ }
}
/// Check if this filter either matches, or does not care about
/// the pubkey/petname tags.
fn pubkey_match(&self, event: &Event) -> bool {
- self.pubkey
- .as_ref()
- .map(|t| event.pubkey_tag_match(t))
- .unwrap_or(true)
+ // This needs to be analyzed for performance; building these
+ // hash sets for each active subscription isn't great.
+ if let Some(ps) = &self.pubkeys {
+ let pubkey_refs =
+ HashSet::<_>::from_iter(event.get_pubkey_tags().iter().map(|x| x.to_owned()));
+ let filter_refs = HashSet::<_>::from_iter(ps.iter().map(|x| &x[..]));
+ let cardinality = pubkey_refs.intersection(&filter_refs).count();
+ cardinality > 0
+ } else {
+ true
+ }
}
/// Check if this filter either matches, or does not care about the kind.
fn kind_match(&self, kind: u64) -> bool {
- self.kind.map(|v| v == kind).unwrap_or(true)
+ self.kinds
+ .as_ref()
+ .map(|ks| ks.contains(&kind))
+ .unwrap_or(true)
}
/// Determine if all populated fields in this filter match the provided event.
pub fn interested_in_event(&self, event: &Event) -> bool {
- self.id.as_ref().map(|v| v == &event.id).unwrap_or(true)
+ // self.id.as_ref().map(|v| v == &event.id).unwrap_or(true)
+ self.ids_match(event)
&& self.since.map(|t| event.created_at > t).unwrap_or(true)
&& self.kind_match(event.kind)
&& self.authors_match(event)