nostr-rs-relay

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

commit 72f8a1aa5c2c1e7ffba2cd897f5c992febc44674
parent 274c61bb723bcd305ed6d3486e6e645c9bbd0d0f
Author: Greg Heartsfield <scsibug@imap.cc>
Date:   Sun, 16 Oct 2022 15:25:06 -0500

feat(NIP-26): allow searches for delegated public keys

Implements core NIP-26 delegated event functionality.  Events can
include a `delegation` tag that provides a signature and restrictions
on which events can be delegated.

Notable points on the implementation so far:

* Schema has been upgraded to include an index and new column.
* Basic rune parsing/evaluation to implement the example event in the
  NIP, but no more.
* No special logic for deletion.
* No migration logic for determining delegated authors for
  already-stored events.

Diffstat:
MCargo.lock | 1+
MCargo.toml | 1+
MREADME.md | 1+
Msrc/db.rs | 22++++++++++++++++------
Asrc/delegation.rs | 416+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/error.rs | 2++
Msrc/event.rs | 81++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
Msrc/info.rs | 2+-
Msrc/lib.rs | 1+
Msrc/schema.rs | 27++++++++++++++++++++++++++-
Msrc/subscription.rs | 24+++++++++++++++++++++++-
11 files changed, 568 insertions(+), 10 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock @@ -1042,6 +1042,7 @@ dependencies = [ "r2d2", "r2d2_sqlite", "rand 0.8.5", + "regex", "rusqlite", "secp256k1", "serde", diff --git a/Cargo.toml b/Cargo.toml @@ -32,6 +32,7 @@ http = { version = "0.2" } parse_duration = "2" rand = "0.8" const_format = "0.2.28" +regex = "1" [dev-dependencies] anyhow = "1" diff --git a/README.md b/README.md @@ -27,6 +27,7 @@ mirrored on [GitHub](https://github.com/scsibug/nostr-rs-relay). - [x] NIP-15: [End of Stored Events Notice](https://github.com/nostr-protocol/nips/blob/master/15.md) - [x] NIP-16: [Event Treatment](https://github.com/nostr-protocol/nips/blob/master/16.md) - [x] NIP-22: [Event `created_at` limits](https://github.com/nostr-protocol/nips/blob/master/22.md) (_future-dated events only_) +- [x] NIP-26: [Event Delegation](https://github.com/nostr-protocol/nips/blob/master/26.md) ## Quick Start diff --git a/src/db.rs b/src/db.rs @@ -142,12 +142,15 @@ pub async fn db_writer( if next_event.is_none() { break; } + // track if an event write occurred; this is used to + // update the rate limiter let mut event_write = false; let subm_event = next_event.unwrap(); let event = subm_event.event; let notice_tx = subm_event.notice_tx; // check if this event is authorized. if let Some(allowed_addrs) = whitelist { + // TODO: incorporate delegated pubkeys // if the event address is not in allowed_addrs. if !allowed_addrs.contains(&event.pubkey) { info!( @@ -284,12 +287,13 @@ pub fn write_event(conn: &mut PooledConnection, e: &Event) -> Result<usize> { let tx = conn.transaction()?; // get relevant fields from event and convert to blobs. let id_blob = hex::decode(&e.id).ok(); - let pubkey_blob = hex::decode(&e.pubkey).ok(); + let pubkey_blob: Option<Vec<u8>> = hex::decode(&e.pubkey).ok(); + let delegator_blob: Option<Vec<u8>> = e.delegated_by.as_ref().and_then(|d| hex::decode(d).ok()); let event_str = serde_json::to_string(&e).ok(); // ignore if the event hash is a duplicate. let mut ins_count = tx.execute( - "INSERT OR IGNORE INTO event (event_hash, created_at, kind, author, content, first_seen, hidden) VALUES (?1, ?2, ?3, ?4, ?5, strftime('%s','now'), FALSE);", - params![id_blob, e.created_at, e.kind, pubkey_blob, event_str] + "INSERT OR IGNORE INTO event (event_hash, created_at, kind, author, delegated_by, content, first_seen, hidden) VALUES (?1, ?2, ?3, ?4, ?5, ?6, strftime('%s','now'), FALSE);", + params![id_blob, e.created_at, e.kind, pubkey_blob, delegator_blob, event_str] )?; if ins_count == 0 { // if the event was a duplicate, no need to insert event or @@ -439,16 +443,22 @@ fn query_from_filter(f: &ReqFilter) -> (String, Vec<Box<dyn ToSql>>) { for auth in authvec { match hex_range(auth) { Some(HexSearch::Exact(ex)) => { - auth_searches.push("author=?".to_owned()); + auth_searches.push("author=? OR delegated_by=?".to_owned()); + params.push(Box::new(ex.clone())); params.push(Box::new(ex)); } Some(HexSearch::Range(lower, upper)) => { - auth_searches.push("(author>? AND author<?)".to_owned()); + auth_searches.push( + "(author>? AND author<?) OR (delegated_by>? AND delegated_by<?)".to_owned(), + ); + params.push(Box::new(lower.clone())); + params.push(Box::new(upper.clone())); params.push(Box::new(lower)); params.push(Box::new(upper)); } Some(HexSearch::LowerOnly(lower)) => { - auth_searches.push("author>?".to_owned()); + auth_searches.push("author>? OR delegated_by>?".to_owned()); + params.push(Box::new(lower.clone())); params.push(Box::new(lower)); } None => { diff --git a/src/delegation.rs b/src/delegation.rs @@ -0,0 +1,416 @@ +//! Event parsing and validation +use crate::error::Error; +use crate::error::Result; +use crate::event::Event; +use bitcoin_hashes::{sha256, Hash}; +use lazy_static::lazy_static; +use regex::Regex; +use secp256k1::{schnorr, Secp256k1, VerifyOnly, XOnlyPublicKey}; +use serde::{Deserialize, Serialize}; +use std::str::FromStr; +use tracing::{debug, info}; + +// This handles everything related to delegation, in particular the +// condition/rune parsing and logic. + +// Conditions are poorly specified, so we will implement the minimum +// necessary for now. + +// fields MUST be either "kind" or "created_at". +// operators supported are ">", "<", "=", "!". +// no operations on 'content' are supported. + +// this allows constraints for: +// valid date ranges (valid from X->Y dates). +// specific kinds (publish kind=1,5) +// kind ranges (publish ephemeral events, kind>19999&kind<30001) + +// for more complex scenarios (allow delegatee to publish ephemeral +// AND replacement events), it may be necessary to generate and use +// different condition strings, since we do not support grouping or +// "OR" logic. + +lazy_static! { + /// Secp256k1 verification instance. + pub static ref SECP: Secp256k1<VerifyOnly> = Secp256k1::verification_only(); +} + +#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Clone)] +pub enum Field { + Kind, + CreatedAt, +} + +impl FromStr for Field { + type Err = Error; + fn from_str(value: &str) -> Result<Self, Self::Err> { + if value == "kind" { + Ok(Field::Kind) + } else if value == "created_at" { + Ok(Field::CreatedAt) + } else { + Err(Error::DelegationParseError) + } + } +} + +#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Clone)] +pub enum Operator { + LessThan, + GreaterThan, + Equals, + NotEquals, +} +impl FromStr for Operator { + type Err = Error; + fn from_str(value: &str) -> Result<Self, Self::Err> { + if value == "<" { + Ok(Operator::LessThan) + } else if value == ">" { + Ok(Operator::GreaterThan) + } else if value == "=" { + Ok(Operator::Equals) + } else if value == "!" { + Ok(Operator::NotEquals) + } else { + Err(Error::DelegationParseError) + } + } +} + +#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Clone)] +pub struct ConditionQuery { + pub(crate) conditions: Vec<Condition>, +} + +impl ConditionQuery { + pub fn allows_event(&self, event: &Event) -> bool { + // check each condition, to ensure that the event complies + // with the restriction. + for c in &self.conditions { + if !c.allows_event(event) { + // any failing conditions invalidates the delegation + // on this event + return false; + } + } + // delegation was permitted unconditionally, or all conditions + // were true + true + } +} + +// Verify that the delegator approved the delegation; return a ConditionQuery if so. +pub fn validate_delegation( + delegator: &str, + delegatee: &str, + cond_query: &str, + sigstr: &str, +) -> Option<ConditionQuery> { + // form the token + let tok = format!("nostr:delegation:{}:{}", delegatee, cond_query); + // form SHA256 hash + let digest: sha256::Hash = sha256::Hash::hash(tok.as_bytes()); + let sig = schnorr::Signature::from_str(sigstr).unwrap(); + if let Ok(msg) = secp256k1::Message::from_slice(digest.as_ref()) { + if let Ok(pubkey) = XOnlyPublicKey::from_str(delegator) { + let verify = SECP.verify_schnorr(&sig, &msg, &pubkey); + if verify.is_ok() { + // return the parsed condition query + cond_query.parse::<ConditionQuery>().ok() + } else { + debug!("client sent an delegation signature that did not validate"); + None + } + } else { + debug!("client sent malformed delegation pubkey"); + None + } + } else { + info!("error converting delegation digest to secp256k1 message"); + None + } +} + +/// Parsed delegation condition +/// see https://github.com/nostr-protocol/nips/pull/28#pullrequestreview-1084903800 +/// An example complex condition would be: kind=1,2,3&created_at<1665265999 +#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Clone)] +pub struct Condition { + pub(crate) field: Field, + pub(crate) operator: Operator, + pub(crate) values: Vec<u64>, +} + +impl Condition { + /// Check if this condition allows the given event to be delegated + pub fn allows_event(&self, event: &Event) -> bool { + // determine what the right-hand side of the operator is + let resolved_field = match &self.field { + Field::Kind => event.kind, + Field::CreatedAt => event.created_at, + }; + match &self.operator { + Operator::LessThan => { + // the less-than operator is only valid for single values. + if self.values.len() == 1 { + if let Some(v) = self.values.first() { + return resolved_field < *v; + } + } + } + Operator::GreaterThan => { + // the greater-than operator is only valid for single values. + if self.values.len() == 1 { + if let Some(v) = self.values.first() { + return resolved_field > *v; + } + } + } + Operator::Equals => { + // equals is interpreted as "must be equal to at least one provided value" + return self.values.iter().any(|&x| resolved_field == x); + } + Operator::NotEquals => { + // not-equals is interpreted as "must not be equal to any provided value" + // this is the one case where an empty list of values could be allowed; even though it is a pointless restriction. + return self.values.iter().all(|&x| resolved_field != x); + } + } + false + } +} + +fn str_to_condition(cs: &str) -> Option<Condition> { + // a condition is a string (alphanum+underscore), an operator (<>=!), and values (num+comma) + lazy_static! { + static ref RE: Regex = Regex::new("([[:word:]]+)([<>=!]+)([,[[:digit:]]]*)").unwrap(); + } + // match against the regex + let caps = RE.captures(cs)?; + let field = caps.get(1)?.as_str().parse::<Field>().ok()?; + let operator = caps.get(2)?.as_str().parse::<Operator>().ok()?; + // values are just comma separated numbers, but all must be parsed + let rawvals = caps.get(3)?.as_str(); + let values = rawvals + .split_terminator(',') + .map(|n| n.parse::<u64>().ok()) + .collect::<Option<Vec<_>>>()?; + // convert field string into Field + Some(Condition { + field, + operator, + values, + }) +} + +/// Parse a condition query from a string slice +impl FromStr for ConditionQuery { + type Err = Error; + fn from_str(value: &str) -> Result<Self, Self::Err> { + // split the string with '&' + let mut conditions = vec![]; + let condstrs = value.split_terminator('&'); + // parse each individual condition + for c in condstrs { + conditions.push(str_to_condition(c).ok_or(Error::DelegationParseError)?); + } + Ok(ConditionQuery { conditions }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + // parse condition strings + #[test] + fn parse_empty() -> Result<()> { + // given an empty condition query, produce an empty vector + let empty_cq = ConditionQuery { conditions: vec![] }; + let parsed = "".parse::<ConditionQuery>()?; + assert_eq!(parsed, empty_cq); + Ok(()) + } + + // parse field 'kind' + #[test] + fn test_kind_field_parse() -> Result<()> { + let field = "kind".parse::<Field>()?; + assert_eq!(field, Field::Kind); + Ok(()) + } + // parse field 'created_at' + #[test] + fn test_created_at_field_parse() -> Result<()> { + let field = "created_at".parse::<Field>()?; + assert_eq!(field, Field::CreatedAt); + Ok(()) + } + // parse unknown field + #[test] + fn unknown_field_parse() { + let field = "unk".parse::<Field>(); + assert!(field.is_err()); + } + + // parse a full conditional query with an empty array + #[test] + fn parse_kind_equals_empty() -> Result<()> { + // given an empty condition query, produce an empty vector + let kind_cq = ConditionQuery { + conditions: vec![Condition { + field: Field::Kind, + operator: Operator::Equals, + values: vec![], + }], + }; + let parsed = "kind=".parse::<ConditionQuery>()?; + assert_eq!(parsed, kind_cq); + Ok(()) + } + // parse a full conditional query with a single value + #[test] + fn parse_kind_equals_singleval() -> Result<()> { + // given an empty condition query, produce an empty vector + let kind_cq = ConditionQuery { + conditions: vec![Condition { + field: Field::Kind, + operator: Operator::Equals, + values: vec![1], + }], + }; + let parsed = "kind=1".parse::<ConditionQuery>()?; + assert_eq!(parsed, kind_cq); + Ok(()) + } + // parse a full conditional query with multiple values + #[test] + fn parse_kind_equals_multival() -> Result<()> { + // given an empty condition query, produce an empty vector + let kind_cq = ConditionQuery { + conditions: vec![Condition { + field: Field::Kind, + operator: Operator::Equals, + values: vec![1, 2, 4], + }], + }; + let parsed = "kind=1,2,4".parse::<ConditionQuery>()?; + assert_eq!(parsed, kind_cq); + Ok(()) + } + // parse multiple conditions + #[test] + fn parse_multi_conditions() -> Result<()> { + // given an empty condition query, produce an empty vector + let cq = ConditionQuery { + conditions: vec![ + Condition { + field: Field::Kind, + operator: Operator::GreaterThan, + values: vec![10000], + }, + Condition { + field: Field::Kind, + operator: Operator::LessThan, + values: vec![20000], + }, + Condition { + field: Field::Kind, + operator: Operator::NotEquals, + values: vec![10001], + }, + Condition { + field: Field::CreatedAt, + operator: Operator::LessThan, + values: vec![1665867123], + }, + ], + }; + let parsed = + "kind>10000&kind<20000&kind!10001&created_at<1665867123".parse::<ConditionQuery>()?; + assert_eq!(parsed, cq); + Ok(()) + } + fn simple_event() -> Event { + Event { + id: "0".to_owned(), + pubkey: "0".to_owned(), + delegated_by: None, + created_at: 0, + kind: 0, + tags: vec![], + content: "".to_owned(), + sig: "0".to_owned(), + tagidx: None, + } + } + // Check for condition logic on event w/ empty values + #[test] + fn condition_with_empty_values() { + let mut c = Condition { + field: Field::Kind, + operator: Operator::GreaterThan, + values: vec![], + }; + let e = simple_event(); + assert!(!c.allows_event(&e)); + c.operator = Operator::LessThan; + assert!(!c.allows_event(&e)); + c.operator = Operator::Equals; + assert!(!c.allows_event(&e)); + // Not Equals applied to an empty list *is* allowed + // (pointless, but logically valid). + c.operator = Operator::NotEquals; + assert!(c.allows_event(&e)); + } + + // Check for condition logic on event w/ single value + #[test] + fn condition_kind_gt_event_single() { + let c = Condition { + field: Field::Kind, + operator: Operator::GreaterThan, + values: vec![10], + }; + let mut e = simple_event(); + // kind is not greater than 10, not allowed + e.kind = 1; + assert!(!c.allows_event(&e)); + // kind is greater than 10, allowed + e.kind = 100; + assert!(c.allows_event(&e)); + // kind is 10, not allowed + e.kind = 10; + assert!(!c.allows_event(&e)); + } + // Check for condition logic on event w/ multi values + #[test] + fn condition_with_multi_values() { + let mut c = Condition { + field: Field::Kind, + operator: Operator::Equals, + values: vec![0, 10, 20], + }; + let mut e = simple_event(); + // Allow if event kind is in list for Equals + e.kind = 10; + assert!(c.allows_event(&e)); + // Deny if event kind is not in list for Equals + e.kind = 11; + assert!(!c.allows_event(&e)); + // Deny if event kind is in list for NotEquals + e.kind = 10; + c.operator = Operator::NotEquals; + assert!(!c.allows_event(&e)); + // Allow if event kind is not in list for NotEquals + e.kind = 99; + c.operator = Operator::NotEquals; + assert!(c.allows_event(&e)); + // Always deny if GreaterThan/LessThan for a list + c.operator = Operator::LessThan; + assert!(!c.allows_event(&e)); + c.operator = Operator::GreaterThan; + assert!(!c.allows_event(&e)); + } +} diff --git a/src/error.rs b/src/error.rs @@ -50,6 +50,8 @@ pub enum Error { HyperError(hyper::Error), #[error("Hex encoding error")] HexError(hex::FromHexError), + #[error("Delegation parse error")] + DelegationParseError, #[error("Unknown/Undocumented")] UnknownError, } diff --git a/src/event.rs b/src/event.rs @@ -1,4 +1,5 @@ //! Event parsing and validation +use crate::delegation::validate_delegation; use crate::error::Error::*; use crate::error::Result; use crate::nip05; @@ -31,6 +32,8 @@ pub struct EventCmd { pub struct Event { pub id: String, pub(crate) pubkey: String, + #[serde(skip)] + pub(crate) delegated_by: Option<String>, pub(crate) created_at: u64, pub(crate) kind: u64, #[serde(deserialize_with = "tag_from_string")] @@ -83,6 +86,7 @@ impl From<EventCmd> for Result<Event> { } else if ec.event.is_valid() { let mut e = ec.event; e.build_index(); + e.update_delegation(); Ok(e) } else { Err(EventInvalid) @@ -110,6 +114,50 @@ impl Event { None } + // is this event delegated (properly)? + // does the signature match, and are conditions valid? + // if so, return an alternate author for the event + pub fn delegated_author(&self) -> Option<String> { + // is there a delegation tag? + let delegation_tag: Vec<String> = self + .tags + .iter() + .filter(|x| x.len() == 4) + .filter(|x| x.get(0).unwrap() == "delegation") + .take(1) + .next()? + .to_vec(); // get first tag + + //let delegation_tag = self.tag_values_by_name("delegation"); + // delegation tags should have exactly 3 elements after the name (pubkey, condition, sig) + // the event is signed by the delagatee + let delegatee = &self.pubkey; + // the delegation tag references the claimed delagator + let delegator: &str = delegation_tag.get(1)?; + let querystr: &str = delegation_tag.get(2)?; + let sig: &str = delegation_tag.get(3)?; + + // attempt to get a condition query; this requires the delegation to have a valid signature. + if let Some(cond_query) = validate_delegation(delegator, delegatee, querystr, sig) { + // The signature was valid, now we ensure the delegation + // condition is valid for this event: + if cond_query.allows_event(self) { + // since this is allowed, we will provide the delegatee + Some(delegator.into()) + } else { + debug!("an event failed to satisfy delegation conditions"); + None + } + } else { + debug!("event had had invalid delegation signature"); + None + } + } + + /// Update delegation status + fn update_delegation(&mut self) { + self.delegated_by = self.delegated_author(); + } /// Build an event tag index fn build_index(&mut self) { // if there are no tags; just leave the index as None @@ -145,7 +193,7 @@ impl Event { self.pubkey.chars().take(8).collect() } - /// Retrieve tag values + /// Retrieve tag initial values across all tags matching the name pub fn tag_values_by_name(&self, tag_name: &str) -> Vec<String> { self.tags .iter() @@ -269,6 +317,7 @@ mod tests { Event { id: "0".to_owned(), pubkey: "0".to_owned(), + delegated_by: None, created_at: 0, kind: 0, tags: vec![], @@ -350,6 +399,7 @@ mod tests { let e = Event { id: "999".to_owned(), pubkey: "012345".to_owned(), + delegated_by: None, created_at: 501234, kind: 1, tags: vec![], @@ -367,6 +417,7 @@ mod tests { let e = Event { id: "999".to_owned(), pubkey: "012345".to_owned(), + delegated_by: None, created_at: 501234, kind: 1, tags: vec![ @@ -389,10 +440,38 @@ mod tests { } #[test] + fn event_no_tag_select() { + let e = Event { + id: "999".to_owned(), + pubkey: "012345".to_owned(), + delegated_by: None, + created_at: 501234, + kind: 1, + tags: vec![ + vec!["j".to_owned(), "abc".to_owned()], + vec!["e".to_owned(), "foo".to_owned()], + vec!["e".to_owned(), "baz".to_owned()], + vec![ + "p".to_owned(), + "aaaa".to_owned(), + "ws://example.com".to_owned(), + ], + ], + content: "this is a test".to_owned(), + sig: "abcde".to_owned(), + tagidx: None, + }; + let v = e.tag_values_by_name("x"); + // asking for tags that don't exist just returns zero-length vector + assert_eq!(v.len(), 0); + } + + #[test] fn event_canonical_with_tags() { let e = Event { id: "999".to_owned(), pubkey: "012345".to_owned(), + delegated_by: None, created_at: 501234, kind: 1, tags: vec![ diff --git a/src/info.rs b/src/info.rs @@ -35,7 +35,7 @@ impl From<config::Info> for RelayInfo { description: i.description, pubkey: i.pubkey, contact: i.contact, - supported_nips: Some(vec![1, 2, 9, 11, 12, 15, 16, 22]), + supported_nips: Some(vec![1, 2, 9, 11, 12, 15, 16, 22, 26]), software: Some("https://git.sr.ht/~gheartsfield/nostr-rs-relay".to_owned()), version: CARGO_PKG_VERSION.map(|x| x.to_owned()), } diff --git a/src/lib.rs b/src/lib.rs @@ -2,6 +2,7 @@ pub mod close; pub mod config; pub mod conn; pub mod db; +pub mod delegation; pub mod error; pub mod event; pub mod hexrange; diff --git a/src/schema.rs b/src/schema.rs @@ -20,7 +20,7 @@ pragma mmap_size = 536870912; -- 512MB of mmap "##; /// Latest database version -pub const DB_VERSION: usize = 6; +pub const DB_VERSION: usize = 7; /// Schema definition const INIT_SQL: &str = formatcp!( @@ -40,6 +40,7 @@ event_hash BLOB NOT NULL, -- 4-byte hash first_seen INTEGER NOT NULL, -- when the event was first seen (not authored!) (seconds since 1970) created_at INTEGER NOT NULL, -- when the event was authored author BLOB NOT NULL, -- author pubkey +delegated_by BLOB, -- delegator pubkey (NIP-26) kind INTEGER NOT NULL, -- event kind hidden INTEGER, -- relevant for queries content TEXT NOT NULL -- serialized json of event object @@ -49,6 +50,7 @@ content TEXT NOT NULL -- serialized json of event object CREATE UNIQUE INDEX IF NOT EXISTS event_hash_index ON event(event_hash); CREATE INDEX IF NOT EXISTS created_at_index ON event(created_at); CREATE INDEX IF NOT EXISTS author_index ON event(author); +CREATE INDEX IF NOT EXISTS delegated_by_index ON event(delegated_by); CREATE INDEX IF NOT EXISTS kind_index ON event(kind); -- Tag Table @@ -152,6 +154,9 @@ pub fn upgrade_db(conn: &mut PooledConnection) -> Result<()> { if curr_version == 5 { curr_version = mig_5_to_6(conn)?; } + if curr_version == 6 { + curr_version = mig_6_to_7(conn)?; + } if curr_version == DB_VERSION { info!( "All migration scripts completed successfully. Welcome to v{}.", @@ -348,3 +353,23 @@ fn mig_5_to_6(conn: &mut PooledConnection) -> Result<usize> { info!("vacuumed DB after tags rebuild in {:?}", start.elapsed()); Ok(6) } + +fn mig_6_to_7(conn: &mut PooledConnection) -> Result<usize> { + info!("database schema needs update from 6->7"); + // only change is adding a hidden column to events. + let upgrade_sql = r##" +ALTER TABLE event ADD delegated_by BLOB; +CREATE INDEX IF NOT EXISTS delegated_by_index ON event(delegated_by); +PRAGMA user_version = 7; +"##; + match conn.execute_batch(upgrade_sql) { + Ok(()) => { + info!("database schema upgraded v6 -> v7"); + } + Err(err) => { + error!("update failed: {}", err); + panic!("database could not be upgraded"); + } + } + Ok(7) +} diff --git a/src/subscription.rs b/src/subscription.rs @@ -217,6 +217,17 @@ impl ReqFilter { .unwrap_or(true) } + fn delegated_authors_match(&self, event: &Event) -> bool { + if let Some(delegated_pubkey) = &event.delegated_by { + self.authors + .as_ref() + .map(|vs| prefix_match(vs, delegated_pubkey)) + .unwrap_or(true) + } else { + false + } + } + fn tag_match(&self, event: &Event) -> bool { // get the hashset from the filter. if let Some(map) = &self.tags { @@ -248,7 +259,7 @@ impl ReqFilter { && self.since.map(|t| event.created_at > t).unwrap_or(true) && self.until.map(|t| event.created_at < t).unwrap_or(true) && self.kind_match(event.kind) - && self.authors_match(event) + && (self.authors_match(event) || self.delegated_authors_match(event)) && self.tag_match(event) && !self.force_no_match } @@ -308,6 +319,7 @@ mod tests { let e = Event { id: "foo".to_owned(), pubkey: "abcd".to_owned(), + delegated_by: None, created_at: 0, kind: 0, tags: Vec::new(), @@ -326,6 +338,7 @@ mod tests { let e = Event { id: "abcd".to_owned(), pubkey: "".to_owned(), + delegated_by: None, created_at: 0, kind: 0, tags: Vec::new(), @@ -344,6 +357,7 @@ mod tests { let e = Event { id: "abcde".to_owned(), pubkey: "".to_owned(), + delegated_by: None, created_at: 0, kind: 0, tags: Vec::new(), @@ -363,6 +377,7 @@ mod tests { let e = Event { id: "abc".to_owned(), pubkey: "".to_owned(), + delegated_by: None, created_at: 50, kind: 0, tags: Vec::new(), @@ -386,6 +401,7 @@ mod tests { let e = Event { id: "abc".to_owned(), pubkey: "".to_owned(), + delegated_by: None, created_at: 150, kind: 0, tags: Vec::new(), @@ -407,6 +423,7 @@ mod tests { let e = Event { id: "abc".to_owned(), pubkey: "".to_owned(), + delegated_by: None, created_at: 50, kind: 0, tags: Vec::new(), @@ -425,6 +442,7 @@ mod tests { let e = Event { id: "abc".to_owned(), pubkey: "".to_owned(), + delegated_by: None, created_at: 1001, kind: 0, tags: Vec::new(), @@ -443,6 +461,7 @@ mod tests { let e = Event { id: "abc".to_owned(), pubkey: "".to_owned(), + delegated_by: None, created_at: 0, kind: 0, tags: Vec::new(), @@ -461,6 +480,7 @@ mod tests { let e = Event { id: "123".to_owned(), pubkey: "abc".to_owned(), + delegated_by: None, created_at: 0, kind: 0, tags: Vec::new(), @@ -479,6 +499,7 @@ mod tests { let e = Event { id: "123".to_owned(), pubkey: "bcd".to_owned(), + delegated_by: None, created_at: 0, kind: 0, tags: Vec::new(), @@ -497,6 +518,7 @@ mod tests { let e = Event { id: "123".to_owned(), pubkey: "xyz".to_owned(), + delegated_by: None, created_at: 0, kind: 0, tags: Vec::new(),