diff --git a/src/event.rs b/src/event.rs index 7399109..8b3529a 100644 --- a/src/event.rs +++ b/src/event.rs @@ -130,6 +130,33 @@ impl Event { self.kind == 0 || self.kind == 3 || self.kind == 41 || (self.kind >= 10000 && self.kind < 20000) } + /// Should this event be replaced with newer timestamps from same author, for distinct `d` tag values? + #[must_use] pub fn is_param_replaceable(&self) -> bool { + self.kind >= 30000 && self.kind < 40000 + } + + /// What is the replaceable `d` tag value? + + /// Should this event be replaced with newer timestamps from same author, for distinct `d` tag values? + #[must_use] pub fn distinct_param(&self) -> Option { + if self.is_param_replaceable() { + let default = "".to_string(); + let dvals:Vec<&String> = self.tags + .iter() + .filter(|x| x.len() >= 1) + .filter(|x| x.get(0).unwrap() == "d") + .map(|x| x.get(1).unwrap_or_else(|| &default)).take(1) + .collect(); + let dval_first = dvals.get(0); + match dval_first { + Some(_) => {dval_first.map(|x| x.to_string())}, + None => Some(default) + } + } else { + None + } + } + /// Pull a NIP-05 Name out of the event, if one exists #[must_use] pub fn get_nip05_addr(&self) -> Option { if self.is_kind_metadata() { @@ -364,7 +391,7 @@ mod tests { fn empty_event_tag_match() { let event = Event::simple_event(); assert!(!event - .generic_tag_val_intersect('e', &HashSet::from(["foo".to_owned(), "bar".to_owned()]))); + .generic_tag_val_intersect('e', &HashSet::from(["foo".to_owned(), "bar".to_owned()]))); } #[test] @@ -509,6 +536,19 @@ mod tests { assert_eq!(c, expected); } + #[test] + fn ephemeral_event() { + let mut event = Event::simple_event(); + event.kind=20000; + assert!(event.is_ephemeral()); + event.kind=29999; + assert!(event.is_ephemeral()); + event.kind=30000; + assert!(!event.is_ephemeral()); + event.kind=19999; + assert!(!event.is_ephemeral()); + } + #[test] fn replaceable_event() { let mut event = Event::simple_event(); @@ -516,9 +556,102 @@ mod tests { assert!(event.is_replaceable()); event.kind=3; assert!(event.is_replaceable()); - event.kind=12000; + event.kind=10000; assert!(event.is_replaceable()); + event.kind=19999; + assert!(event.is_replaceable()); + event.kind=20000; + assert!(!event.is_replaceable()); + } + #[test] + fn param_replaceable_event() { + let mut event = Event::simple_event(); + event.kind = 30000; + assert!(event.is_param_replaceable()); + event.kind = 39999; + assert!(event.is_param_replaceable()); + event.kind = 29999; + assert!(!event.is_param_replaceable()); + event.kind = 40000; + assert!(!event.is_param_replaceable()); + } + + #[test] + fn param_replaceable_value_case_1() { + // NIP case #1: "tags":[["d",""]] + let mut event = Event::simple_event(); + event.kind = 30000; + event.tags = vec![ + vec!["d".to_owned(), "".to_owned()]]; + assert_eq!(event.distinct_param(), Some("".to_string())); + } + + #[test] + fn param_replaceable_value_case_2() { + // NIP case #2: "tags":[]: implicit d tag with empty value + let mut event = Event::simple_event(); + event.kind = 30000; + assert_eq!(event.distinct_param(), Some("".to_string())); + } + + #[test] + fn param_replaceable_value_case_3() { + // NIP case #3: "tags":[["d"]]: implicit empty value "" + let mut event = Event::simple_event(); + event.kind = 30000; + event.tags = vec![ + vec!["d".to_owned()]]; + assert_eq!(event.distinct_param(), Some("".to_string())); + } + + #[test] + fn param_replaceable_value_case_4() { + // NIP case #4: "tags":[["d",""],["d","not empty"]]: only first d tag is considered + let mut event = Event::simple_event(); + event.kind = 30000; + event.tags = vec![ + vec!["d".to_owned(), "".to_string()], + vec!["d".to_owned(), "not empty".to_string()] + ]; + assert_eq!(event.distinct_param(), Some("".to_string())); + } + + #[test] + fn param_replaceable_value_case_4b() { + // Variation of #4 with + // NIP case #4: "tags":[["d","not empty"],["d",""]]: only first d tag is considered + let mut event = Event::simple_event(); + event.kind = 30000; + event.tags = vec![ + vec!["d".to_owned(), "not empty".to_string()], + vec!["d".to_owned(), "".to_string()] + ]; + assert_eq!(event.distinct_param(), Some("not empty".to_string())); + } + + #[test] + fn param_replaceable_value_case_5() { + // NIP case #5: "tags":[["d"],["d","some value"]]: only first d tag is considered + let mut event = Event::simple_event(); + event.kind = 30000; + event.tags = vec![ + vec!["d".to_owned()], + vec!["d".to_owned(), "second value".to_string()], + vec!["d".to_owned(), "third value".to_string()] + ]; + assert_eq!(event.distinct_param(), Some("".to_string())); + } + + #[test] + fn param_replaceable_value_case_6() { + // NIP case #6: "tags":[["e"]]: same as no tags + let mut event = Event::simple_event(); + event.kind = 30000; + event.tags = vec![ + vec!["e".to_owned()], + ]; + assert_eq!(event.distinct_param(), Some("".to_string())); } } diff --git a/src/repo/sqlite.rs b/src/repo/sqlite.rs index 8ac92a9..ff5fb75 100644 --- a/src/repo/sqlite.rs +++ b/src/repo/sqlite.rs @@ -115,6 +115,25 @@ impl SqliteRepo { return Ok(0); } } + // check for parameterized replaceable events that would be hidden; don't insert these either. + if let Some(d_tag) = e.distinct_param() { + let repl_count; + if is_lower_hex(&d_tag) && (d_tag.len() % 2 == 0) { + repl_count = tx.query_row( + "SELECT e.id FROM event e LEFT JOIN tag t ON e.id=t.event_id WHERE e.author=? AND e.kind=? AND t.name='d' AND t.value_hex=? AND e.created_at >= ? LIMIT 1;", + params![pubkey_blob, e.kind, hex::decode(d_tag).ok(), e.created_at],|row| row.get::(0)); + } else { + repl_count = tx.query_row( + "SELECT e.id FROM event e LEFT JOIN tag t ON e.id=t.event_id WHERE e.author=? AND e.kind=? AND t.name='d' AND t.value=? AND e.created_at >= ? LIMIT 1;", + params![pubkey_blob, e.kind, d_tag, e.created_at],|row| row.get::(0)); + } + // if any rows were returned, then some newer event with + // the same author/kind/tag value exist, and we can ignore + // this event. + if repl_count.ok().is_some() { + return Ok(0) + } + } // 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, delegated_by, content, first_seen, hidden) VALUES (?1, ?2, ?3, ?4, ?5, ?6, strftime('%s','now'), FALSE);", @@ -174,6 +193,27 @@ impl SqliteRepo { ); } } + // if this event is parameterized replaceable, remove other events. + if let Some(d_tag) = e.distinct_param() { + let update_count; + if is_lower_hex(&d_tag) && (d_tag.len() % 2 == 0) { + update_count = tx.execute( + "DELETE FROM event WHERE kind=? AND author=? AND id NOT IN (SELECT e.id FROM event e LEFT JOIN tag t ON e.id=t.event_id WHERE e.kind=? AND e.author=? AND t.name='d' AND t.value_hex=? ORDER BY created_at DESC LIMIT 1);", + params![e.kind, pubkey_blob, e.kind, pubkey_blob, hex::decode(d_tag).ok()])?; + } else { + update_count = tx.execute( + "DELETE FROM event WHERE kind=? AND author=? AND id NOT IN (SELECT e.id FROM event e LEFT JOIN tag t ON e.id=t.event_id WHERE e.kind=? AND e.author=? AND t.name='d' AND t.value=? ORDER BY created_at DESC LIMIT 1);", + params![e.kind, pubkey_blob, e.kind, pubkey_blob, d_tag])?; + } + if update_count > 0 { + info!( + "removed {} older parameterized replaceable kind {} events for author: {:?}", + update_count, + e.kind, + e.get_author_prefix() + ); + } + } // if this event is a deletion, hide the referenced events from the same author. if e.kind == 5 { let event_candidates = e.tag_values_by_name("e");