nostr-rs-relay

My dev fork of nostr-rs-relay
git clone git://jb55.com/nostr-rs-relay
Log | Files | Refs | README | LICENSE

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:
Msrc/db.rs | 59++++++++++++++++++++++++++++++++++++++---------------------
Msrc/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)