2021-12-11 22:43:41 -05:00
|
|
|
//! Event parsing and validation
|
2022-10-16 16:25:06 -04:00
|
|
|
use crate::delegation::validate_delegation;
|
2023-02-08 10:55:17 -05:00
|
|
|
use crate::error::Error::{
|
|
|
|
CommandUnknownError, EventCouldNotCanonicalize, EventInvalidId, EventInvalidSignature,
|
|
|
|
EventMalformedPubkey,
|
|
|
|
};
|
2021-12-05 17:53:26 -05:00
|
|
|
use crate::error::Result;
|
2023-02-25 15:49:35 -05:00
|
|
|
use crate::event::EventWrapper::WrappedAuth;
|
|
|
|
use crate::event::EventWrapper::WrappedEvent;
|
2022-02-12 10:29:25 -05:00
|
|
|
use crate::nip05;
|
2022-02-12 10:29:38 -05:00
|
|
|
use crate::utils::unix_time;
|
2021-12-05 17:53:26 -05:00
|
|
|
use bitcoin_hashes::{sha256, Hash};
|
2022-01-01 10:08:19 -05:00
|
|
|
use lazy_static::lazy_static;
|
|
|
|
use secp256k1::{schnorr, Secp256k1, VerifyOnly, XOnlyPublicKey};
|
2021-12-05 17:53:26 -05:00
|
|
|
use serde::{Deserialize, Deserializer, Serialize};
|
|
|
|
use serde_json::value::Value;
|
|
|
|
use serde_json::Number;
|
2022-01-05 17:33:53 -05:00
|
|
|
use std::collections::HashMap;
|
|
|
|
use std::collections::HashSet;
|
2021-12-05 17:53:26 -05:00
|
|
|
use std::str::FromStr;
|
2022-09-28 08:19:59 -04:00
|
|
|
use tracing::{debug, info};
|
2021-12-05 17:53:26 -05:00
|
|
|
|
2022-01-01 10:08:19 -05:00
|
|
|
lazy_static! {
|
2022-02-12 10:29:35 -05:00
|
|
|
/// Secp256k1 verification instance.
|
2022-01-01 10:08:19 -05:00
|
|
|
pub static ref SECP: Secp256k1<VerifyOnly> = Secp256k1::verification_only();
|
|
|
|
}
|
|
|
|
|
2022-02-12 10:29:35 -05:00
|
|
|
/// Event command in network format.
|
2022-09-24 09:30:22 -04:00
|
|
|
#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Clone)]
|
2021-12-05 17:53:26 -05:00
|
|
|
pub struct EventCmd {
|
|
|
|
cmd: String, // expecting static "EVENT"
|
|
|
|
event: Event,
|
|
|
|
}
|
|
|
|
|
2022-11-11 10:20:36 -05:00
|
|
|
impl EventCmd {
|
2023-02-08 10:55:17 -05:00
|
|
|
#[must_use]
|
|
|
|
pub fn event_id(&self) -> &str {
|
2022-11-12 10:22:24 -05:00
|
|
|
&self.event.id
|
2022-11-11 10:20:36 -05:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-02-12 10:29:35 -05:00
|
|
|
/// Parsed nostr event.
|
2022-09-24 09:30:22 -04:00
|
|
|
#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Clone)]
|
2021-12-05 17:53:26 -05:00
|
|
|
pub struct Event {
|
2021-12-05 19:14:14 -05:00
|
|
|
pub id: String,
|
2022-12-17 02:52:34 -05:00
|
|
|
pub pubkey: String,
|
2022-10-16 16:25:06 -04:00
|
|
|
#[serde(skip)]
|
2022-12-17 02:52:34 -05:00
|
|
|
pub delegated_by: Option<String>,
|
|
|
|
pub created_at: u64,
|
|
|
|
pub kind: u64,
|
2021-12-05 17:53:26 -05:00
|
|
|
#[serde(deserialize_with = "tag_from_string")]
|
2021-12-11 22:43:41 -05:00
|
|
|
// NOTE: array-of-arrays may need to be more general than a string container
|
2022-12-17 02:52:34 -05:00
|
|
|
pub tags: Vec<Vec<String>>,
|
|
|
|
pub content: String,
|
|
|
|
pub sig: String,
|
2022-08-07 11:15:36 -04:00
|
|
|
// Optimization for tag search, built on demand.
|
2022-01-05 17:33:53 -05:00
|
|
|
#[serde(skip)]
|
2022-12-17 02:52:34 -05:00
|
|
|
pub tagidx: Option<HashMap<char, HashSet<String>>>,
|
2021-12-05 17:53:26 -05:00
|
|
|
}
|
|
|
|
|
2021-12-11 22:43:41 -05:00
|
|
|
/// Simple tag type for array of array of strings.
|
2021-12-05 17:53:26 -05:00
|
|
|
type Tag = Vec<Vec<String>>;
|
|
|
|
|
2021-12-11 22:43:41 -05:00
|
|
|
/// Deserializer that ensures we always have a [`Tag`].
|
2021-12-05 17:53:26 -05:00
|
|
|
fn tag_from_string<'de, D>(deserializer: D) -> Result<Tag, D::Error>
|
|
|
|
where
|
|
|
|
D: Deserializer<'de>,
|
|
|
|
{
|
|
|
|
let opt = Option::deserialize(deserializer)?;
|
2022-09-02 13:26:00 -04:00
|
|
|
Ok(opt.unwrap_or_default())
|
2021-12-05 17:53:26 -05:00
|
|
|
}
|
|
|
|
|
2022-08-07 11:15:36 -04:00
|
|
|
/// Attempt to form a single-char tag name.
|
2023-02-08 10:55:17 -05:00
|
|
|
#[must_use]
|
|
|
|
pub fn single_char_tagname(tagname: &str) -> Option<char> {
|
2022-08-07 11:15:36 -04:00
|
|
|
// We return the tag character if and only if the tagname consists
|
|
|
|
// of a single char.
|
|
|
|
let mut tagnamechars = tagname.chars();
|
|
|
|
let firstchar = tagnamechars.next();
|
2022-09-02 13:26:00 -04:00
|
|
|
match firstchar {
|
2022-08-07 11:15:36 -04:00
|
|
|
Some(_) => {
|
|
|
|
// check second char
|
|
|
|
if tagnamechars.next().is_none() {
|
|
|
|
firstchar
|
|
|
|
} else {
|
|
|
|
None
|
|
|
|
}
|
|
|
|
}
|
|
|
|
None => None,
|
2022-09-02 13:26:00 -04:00
|
|
|
}
|
2022-08-07 11:15:36 -04:00
|
|
|
}
|
|
|
|
|
2023-02-14 22:17:48 -05:00
|
|
|
pub enum EventWrapper {
|
|
|
|
WrappedEvent(Event),
|
2023-02-25 15:49:35 -05:00
|
|
|
WrappedAuth(Event),
|
2023-02-14 22:17:48 -05:00
|
|
|
}
|
|
|
|
|
2021-12-11 22:43:41 -05:00
|
|
|
/// Convert network event to parsed/validated event.
|
2023-02-14 22:17:48 -05:00
|
|
|
impl From<EventCmd> for Result<EventWrapper> {
|
|
|
|
fn from(ec: EventCmd) -> Result<EventWrapper> {
|
2021-12-05 17:53:26 -05:00
|
|
|
// ensure command is correct
|
2023-01-22 10:49:49 -05:00
|
|
|
if ec.cmd == "EVENT" {
|
2023-01-22 11:06:44 -05:00
|
|
|
ec.event.validate().map(|_| {
|
2022-11-11 10:20:36 -05:00
|
|
|
let mut e = ec.event;
|
|
|
|
e.build_index();
|
|
|
|
e.update_delegation();
|
2023-02-14 22:17:48 -05:00
|
|
|
WrappedEvent(e)
|
2022-11-11 10:20:36 -05:00
|
|
|
})
|
2023-02-14 22:17:48 -05:00
|
|
|
} else if ec.cmd == "AUTH" {
|
|
|
|
// we don't want to validate the event here, because NIP-42 can be disabled
|
|
|
|
// it will be validated later during the authentication process
|
|
|
|
Ok(WrappedAuth(ec.event))
|
2023-01-22 10:49:49 -05:00
|
|
|
} else {
|
2023-01-22 11:06:44 -05:00
|
|
|
Err(CommandUnknownError)
|
2021-12-05 17:53:26 -05:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl Event {
|
2022-12-21 02:59:04 -05:00
|
|
|
#[cfg(test)]
|
2023-02-08 10:55:17 -05:00
|
|
|
#[must_use]
|
|
|
|
pub fn simple_event() -> Event {
|
2022-12-21 02:59:04 -05:00
|
|
|
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,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-02-08 10:55:17 -05:00
|
|
|
#[must_use]
|
|
|
|
pub fn is_kind_metadata(&self) -> bool {
|
2022-02-12 10:29:25 -05:00
|
|
|
self.kind == 0
|
|
|
|
}
|
|
|
|
|
2023-01-24 09:04:42 -05:00
|
|
|
/// Should this event be persisted?
|
2023-02-08 10:55:17 -05:00
|
|
|
#[must_use]
|
|
|
|
pub fn is_ephemeral(&self) -> bool {
|
2023-01-24 09:04:42 -05:00
|
|
|
self.kind >= 20000 && self.kind < 30000
|
|
|
|
}
|
|
|
|
|
2023-02-17 12:15:06 -05:00
|
|
|
/// Is this event currently expired?
|
|
|
|
pub fn is_expired(&self) -> bool {
|
|
|
|
if let Some(exp) = self.expiration() {
|
|
|
|
exp <= unix_time()
|
|
|
|
} else {
|
|
|
|
false
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-02-15 19:47:27 -05:00
|
|
|
/// Determine the time at which this event should expire
|
|
|
|
pub fn expiration(&self) -> Option<u64> {
|
|
|
|
let default = "".to_string();
|
2023-02-25 15:49:35 -05:00
|
|
|
let dvals: Vec<&String> = self
|
|
|
|
.tags
|
2023-02-15 19:47:27 -05:00
|
|
|
.iter()
|
|
|
|
.filter(|x| !x.is_empty())
|
|
|
|
.filter(|x| x.get(0).unwrap() == "expiration")
|
2023-02-25 15:49:35 -05:00
|
|
|
.map(|x| x.get(1).unwrap_or(&default))
|
|
|
|
.take(1)
|
2023-02-15 19:47:27 -05:00
|
|
|
.collect();
|
|
|
|
let val_first = dvals.get(0);
|
|
|
|
val_first.and_then(|t| t.parse::<u64>().ok())
|
|
|
|
}
|
|
|
|
|
2023-01-15 10:18:53 -05:00
|
|
|
/// Should this event be replaced with newer timestamps from same author?
|
2023-02-08 10:55:17 -05:00
|
|
|
#[must_use]
|
|
|
|
pub fn is_replaceable(&self) -> bool {
|
|
|
|
self.kind == 0
|
|
|
|
|| self.kind == 3
|
|
|
|
|| self.kind == 41
|
|
|
|
|| (self.kind >= 10000 && self.kind < 20000)
|
2023-01-15 10:18:53 -05:00
|
|
|
}
|
|
|
|
|
2023-01-24 09:04:42 -05:00
|
|
|
/// Should this event be replaced with newer timestamps from same author, for distinct `d` tag values?
|
2023-02-08 10:55:17 -05:00
|
|
|
#[must_use]
|
|
|
|
pub fn is_param_replaceable(&self) -> bool {
|
2023-01-24 09:04:42 -05:00
|
|
|
self.kind >= 30000 && self.kind < 40000
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Should this event be replaced with newer timestamps from same author, for distinct `d` tag values?
|
2023-02-08 10:55:17 -05:00
|
|
|
#[must_use]
|
|
|
|
pub fn distinct_param(&self) -> Option<String> {
|
2023-01-24 09:04:42 -05:00
|
|
|
if self.is_param_replaceable() {
|
|
|
|
let default = "".to_string();
|
2023-02-08 10:55:17 -05:00
|
|
|
let dvals: Vec<&String> = self
|
|
|
|
.tags
|
2023-01-24 09:04:42 -05:00
|
|
|
.iter()
|
2023-02-06 07:43:09 -05:00
|
|
|
.filter(|x| !x.is_empty())
|
2023-01-24 09:04:42 -05:00
|
|
|
.filter(|x| x.get(0).unwrap() == "d")
|
2023-02-08 10:55:17 -05:00
|
|
|
.map(|x| x.get(1).unwrap_or(&default))
|
|
|
|
.take(1)
|
2023-01-24 09:04:42 -05:00
|
|
|
.collect();
|
|
|
|
let dval_first = dvals.get(0);
|
|
|
|
match dval_first {
|
2023-02-08 10:55:17 -05:00
|
|
|
Some(_) => dval_first.map(|x| x.to_string()),
|
|
|
|
None => Some(default),
|
2023-01-24 09:04:42 -05:00
|
|
|
}
|
|
|
|
} else {
|
|
|
|
None
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-02-12 10:29:25 -05:00
|
|
|
/// Pull a NIP-05 Name out of the event, if one exists
|
2023-02-08 10:55:17 -05:00
|
|
|
#[must_use]
|
|
|
|
pub fn get_nip05_addr(&self) -> Option<nip05::Nip05Name> {
|
2022-02-12 10:29:25 -05:00
|
|
|
if self.is_kind_metadata() {
|
|
|
|
// very quick check if we should attempt to parse this json
|
|
|
|
if self.content.contains("\"nip05\"") {
|
|
|
|
// Parse into JSON
|
|
|
|
let md_parsed: Value = serde_json::from_str(&self.content).ok()?;
|
|
|
|
let md_map = md_parsed.as_object()?;
|
|
|
|
let nip05_str = md_map.get("nip05")?.as_str()?;
|
|
|
|
return nip05::Nip05Name::try_from(nip05_str).ok();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
None
|
|
|
|
}
|
|
|
|
|
2022-10-16 16:25:06 -04:00
|
|
|
// is this event delegated (properly)?
|
|
|
|
// does the signature match, and are conditions valid?
|
|
|
|
// if so, return an alternate author for the event
|
2023-02-08 10:55:17 -05:00
|
|
|
#[must_use]
|
|
|
|
pub fn delegated_author(&self) -> Option<String> {
|
2022-10-16 16:25:06 -04:00
|
|
|
// 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)
|
2023-02-08 10:55:17 -05:00
|
|
|
.next()?
|
|
|
|
.clone(); // get first tag
|
2022-10-16 16:25:06 -04:00
|
|
|
|
|
|
|
//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
|
2023-01-06 07:57:56 -05:00
|
|
|
pub fn update_delegation(&mut self) {
|
2022-10-16 16:25:06 -04:00
|
|
|
self.delegated_by = self.delegated_author();
|
|
|
|
}
|
2022-01-05 17:33:53 -05:00
|
|
|
/// Build an event tag index
|
2023-01-06 07:57:56 -05:00
|
|
|
pub fn build_index(&mut self) {
|
2022-01-05 17:33:53 -05:00
|
|
|
// if there are no tags; just leave the index as None
|
|
|
|
if self.tags.is_empty() {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
// otherwise, build an index
|
2022-08-07 11:15:36 -04:00
|
|
|
let mut idx: HashMap<char, HashSet<String>> = HashMap::new();
|
2022-01-05 17:33:53 -05:00
|
|
|
// iterate over tags that have at least 2 elements
|
|
|
|
for t in self.tags.iter().filter(|x| x.len() > 1) {
|
|
|
|
let tagname = t.get(0).unwrap();
|
2022-08-07 11:15:36 -04:00
|
|
|
let tagnamechar_opt = single_char_tagname(tagname);
|
|
|
|
if tagnamechar_opt.is_none() {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
let tagnamechar = tagnamechar_opt.unwrap();
|
2022-01-05 17:33:53 -05:00
|
|
|
let tagval = t.get(1).unwrap();
|
|
|
|
// ensure a vector exists for this tag
|
2022-09-02 13:38:31 -04:00
|
|
|
idx.entry(tagnamechar).or_insert_with(HashSet::new);
|
2022-01-05 17:33:53 -05:00
|
|
|
// get the tag vec and insert entry
|
2022-09-24 10:01:09 -04:00
|
|
|
let idx_tag_vec = idx.get_mut(&tagnamechar).expect("could not get tag vector");
|
|
|
|
idx_tag_vec.insert(tagval.clone());
|
2022-01-05 17:33:53 -05:00
|
|
|
}
|
|
|
|
// save the tag structure
|
|
|
|
self.tagidx = Some(idx);
|
|
|
|
}
|
|
|
|
|
2021-12-11 22:43:41 -05:00
|
|
|
/// Create a short event identifier, suitable for logging.
|
2023-02-08 10:55:17 -05:00
|
|
|
#[must_use]
|
|
|
|
pub fn get_event_id_prefix(&self) -> String {
|
2021-12-05 21:28:02 -05:00
|
|
|
self.id.chars().take(8).collect()
|
|
|
|
}
|
2023-02-08 10:55:17 -05:00
|
|
|
#[must_use]
|
|
|
|
pub fn get_author_prefix(&self) -> String {
|
2022-02-12 10:29:25 -05:00
|
|
|
self.pubkey.chars().take(8).collect()
|
|
|
|
}
|
2021-12-05 21:28:02 -05:00
|
|
|
|
2022-10-16 16:25:06 -04:00
|
|
|
/// Retrieve tag initial values across all tags matching the name
|
2023-02-08 10:55:17 -05:00
|
|
|
#[must_use]
|
|
|
|
pub fn tag_values_by_name(&self, tag_name: &str) -> Vec<String> {
|
2022-02-27 12:34:10 -05:00
|
|
|
self.tags
|
|
|
|
.iter()
|
|
|
|
.filter(|x| x.len() > 1)
|
|
|
|
.filter(|x| x.get(0).unwrap() == tag_name)
|
2023-01-22 10:49:49 -05:00
|
|
|
.map(|x| x.get(1).unwrap().clone())
|
2022-02-27 12:34:10 -05:00
|
|
|
.collect()
|
|
|
|
}
|
|
|
|
|
2023-02-08 10:55:17 -05:00
|
|
|
#[must_use]
|
|
|
|
pub fn is_valid_timestamp(&self, reject_future_seconds: Option<usize>) -> bool {
|
2022-09-06 07:12:07 -04:00
|
|
|
if let Some(allowable_future) = reject_future_seconds {
|
2021-12-29 23:47:31 -05:00
|
|
|
let curr_time = unix_time();
|
|
|
|
// calculate difference, plus how far future we allow
|
|
|
|
if curr_time + (allowable_future as u64) < self.created_at {
|
|
|
|
let delta = self.created_at - curr_time;
|
2021-12-30 07:35:36 -05:00
|
|
|
debug!(
|
2022-02-27 20:30:48 -05:00
|
|
|
"event is too far in the future ({} seconds), rejecting",
|
2021-12-29 23:47:31 -05:00
|
|
|
delta
|
|
|
|
);
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
2022-09-06 07:12:07 -04:00
|
|
|
true
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Check if this event has a valid signature.
|
2022-12-17 02:52:34 -05:00
|
|
|
pub fn validate(&self) -> Result<()> {
|
2022-09-06 07:12:07 -04:00
|
|
|
// TODO: return a Result with a reason for invalid events
|
2021-12-05 17:53:26 -05:00
|
|
|
// validation is performed by:
|
|
|
|
// * parsing JSON string into event fields
|
|
|
|
// * create an array:
|
|
|
|
// ** [0, pubkey-hex-string, created-at-num, kind-num, tags-array-of-arrays, content-string]
|
|
|
|
// * serialize with no spaces/newlines
|
|
|
|
let c_opt = self.to_canonical();
|
|
|
|
if c_opt.is_none() {
|
2022-11-11 10:20:36 -05:00
|
|
|
debug!("could not canonicalize");
|
|
|
|
return Err(EventCouldNotCanonicalize);
|
2021-12-05 17:53:26 -05:00
|
|
|
}
|
|
|
|
let c = c_opt.unwrap();
|
|
|
|
// * compute the sha256sum.
|
2021-12-11 22:56:52 -05:00
|
|
|
let digest: sha256::Hash = sha256::Hash::hash(c.as_bytes());
|
2023-02-06 07:43:09 -05:00
|
|
|
let hex_digest = format!("{digest:x}");
|
2021-12-05 17:53:26 -05:00
|
|
|
// * ensure the id matches the computed sha256sum.
|
|
|
|
if self.id != hex_digest {
|
2022-02-12 10:29:25 -05:00
|
|
|
debug!("event id does not match digest");
|
2022-11-11 10:20:36 -05:00
|
|
|
return Err(EventInvalidId);
|
2021-12-05 17:53:26 -05:00
|
|
|
}
|
|
|
|
// * validate the message digest (sig) using the pubkey & computed sha256 message hash.
|
2022-01-01 10:08:19 -05:00
|
|
|
let sig = schnorr::Signature::from_str(&self.sig).unwrap();
|
|
|
|
if let Ok(msg) = secp256k1::Message::from_slice(digest.as_ref()) {
|
2022-01-29 14:19:34 -05:00
|
|
|
if let Ok(pubkey) = XOnlyPublicKey::from_str(&self.pubkey) {
|
2022-11-11 10:20:36 -05:00
|
|
|
SECP.verify_schnorr(&sig, &msg, &pubkey)
|
|
|
|
.map_err(|_| EventInvalidSignature)
|
2022-01-29 14:19:34 -05:00
|
|
|
} else {
|
2022-02-27 20:30:48 -05:00
|
|
|
debug!("client sent malformed pubkey");
|
2022-11-11 10:20:36 -05:00
|
|
|
Err(EventMalformedPubkey)
|
2022-01-29 14:19:34 -05:00
|
|
|
}
|
2022-01-01 10:08:19 -05:00
|
|
|
} else {
|
2022-02-27 20:30:48 -05:00
|
|
|
info!("error converting digest to secp256k1 message");
|
2022-11-11 10:20:36 -05:00
|
|
|
Err(EventInvalidSignature)
|
2022-01-01 10:08:19 -05:00
|
|
|
}
|
2021-12-05 17:53:26 -05:00
|
|
|
}
|
|
|
|
|
2021-12-11 22:43:41 -05:00
|
|
|
/// Convert event to canonical representation for signing.
|
2023-02-14 22:17:48 -05:00
|
|
|
pub fn to_canonical(&self) -> Option<String> {
|
2021-12-05 17:53:26 -05:00
|
|
|
// create a JsonValue for each event element
|
|
|
|
let mut c: Vec<Value> = vec![];
|
|
|
|
// id must be set to 0
|
2021-12-11 22:56:52 -05:00
|
|
|
let id = Number::from(0_u64);
|
2021-12-05 17:53:26 -05:00
|
|
|
c.push(serde_json::Value::Number(id));
|
|
|
|
// public key
|
2023-01-22 10:49:49 -05:00
|
|
|
c.push(Value::String(self.pubkey.clone()));
|
2021-12-05 17:53:26 -05:00
|
|
|
// creation time
|
|
|
|
let created_at = Number::from(self.created_at);
|
|
|
|
c.push(serde_json::Value::Number(created_at));
|
|
|
|
// kind
|
|
|
|
let kind = Number::from(self.kind);
|
|
|
|
c.push(serde_json::Value::Number(kind));
|
|
|
|
// tags
|
|
|
|
c.push(self.tags_to_canonical());
|
|
|
|
// content
|
2023-01-22 10:49:49 -05:00
|
|
|
c.push(Value::String(self.content.clone()));
|
2021-12-05 17:53:26 -05:00
|
|
|
serde_json::to_string(&Value::Array(c)).ok()
|
|
|
|
}
|
2021-12-11 22:43:41 -05:00
|
|
|
|
|
|
|
/// Convert tags to a canonical form for signing.
|
2021-12-05 17:53:26 -05:00
|
|
|
fn tags_to_canonical(&self) -> Value {
|
|
|
|
let mut tags = Vec::<Value>::new();
|
|
|
|
// iterate over self tags,
|
2023-01-22 10:49:49 -05:00
|
|
|
for t in &self.tags {
|
2021-12-05 17:53:26 -05:00
|
|
|
// each tag is a vec of strings
|
|
|
|
let mut a = Vec::<Value>::new();
|
|
|
|
for v in t.iter() {
|
2023-01-22 10:49:49 -05:00
|
|
|
a.push(serde_json::Value::String(v.clone()));
|
2021-12-05 17:53:26 -05:00
|
|
|
}
|
|
|
|
tags.push(serde_json::Value::Array(a));
|
|
|
|
}
|
|
|
|
serde_json::Value::Array(tags)
|
|
|
|
}
|
|
|
|
|
2022-01-05 17:33:53 -05:00
|
|
|
/// Determine if the given tag and value set intersect with tags in this event.
|
2023-02-08 10:55:17 -05:00
|
|
|
#[must_use]
|
|
|
|
pub fn generic_tag_val_intersect(&self, tagname: char, check: &HashSet<String>) -> bool {
|
2022-01-05 17:33:53 -05:00
|
|
|
match &self.tagidx {
|
2022-08-07 11:15:36 -04:00
|
|
|
// check if this is indexable tagname
|
|
|
|
Some(idx) => match idx.get(&tagname) {
|
2022-01-05 17:33:53 -05:00
|
|
|
Some(valset) => {
|
|
|
|
let common = valset.intersection(check);
|
|
|
|
common.count() > 0
|
|
|
|
}
|
|
|
|
None => false,
|
|
|
|
},
|
|
|
|
None => false,
|
|
|
|
}
|
|
|
|
}
|
2021-12-05 17:53:26 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
mod tests {
|
|
|
|
use super::*;
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn event_creation() {
|
|
|
|
// create an event
|
2022-12-21 02:59:04 -05:00
|
|
|
let event = Event::simple_event();
|
2021-12-05 17:53:26 -05:00
|
|
|
assert_eq!(event.id, "0");
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn event_serialize() -> Result<()> {
|
|
|
|
// serialize an event to JSON string
|
2022-12-21 02:59:04 -05:00
|
|
|
let event = Event::simple_event();
|
2021-12-05 17:53:26 -05:00
|
|
|
let j = serde_json::to_string(&event)?;
|
|
|
|
assert_eq!(j, "{\"id\":\"0\",\"pubkey\":\"0\",\"created_at\":0,\"kind\":0,\"tags\":[],\"content\":\"\",\"sig\":\"0\"}");
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
|
2021-12-11 16:48:59 -05:00
|
|
|
#[test]
|
2022-09-24 09:39:41 -04:00
|
|
|
fn empty_event_tag_match() {
|
2022-12-21 02:59:04 -05:00
|
|
|
let event = Event::simple_event();
|
2022-01-25 19:21:43 -05:00
|
|
|
assert!(!event
|
2023-02-08 10:55:17 -05:00
|
|
|
.generic_tag_val_intersect('e', &HashSet::from(["foo".to_owned(), "bar".to_owned()])));
|
2021-12-11 16:48:59 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
2022-09-24 09:39:41 -04:00
|
|
|
fn single_event_tag_match() {
|
2022-12-21 02:59:04 -05:00
|
|
|
let mut event = Event::simple_event();
|
2021-12-11 16:48:59 -05:00
|
|
|
event.tags = vec![vec!["e".to_owned(), "foo".to_owned()]];
|
2022-01-25 19:21:43 -05:00
|
|
|
event.build_index();
|
|
|
|
assert_eq!(
|
|
|
|
event.generic_tag_val_intersect(
|
2022-08-17 19:34:11 -04:00
|
|
|
'e',
|
2022-01-25 19:21:43 -05:00
|
|
|
&HashSet::from(["foo".to_owned(), "bar".to_owned()])
|
|
|
|
),
|
|
|
|
true
|
|
|
|
);
|
2021-12-11 16:48:59 -05:00
|
|
|
}
|
|
|
|
|
2021-12-05 17:53:26 -05:00
|
|
|
#[test]
|
|
|
|
fn event_tags_serialize() -> Result<()> {
|
|
|
|
// serialize an event with tags to JSON string
|
2022-12-21 02:59:04 -05:00
|
|
|
let mut event = Event::simple_event();
|
2021-12-05 17:53:26 -05:00
|
|
|
event.tags = vec![
|
|
|
|
vec![
|
|
|
|
"e".to_owned(),
|
|
|
|
"xxxx".to_owned(),
|
|
|
|
"wss://example.com".to_owned(),
|
|
|
|
],
|
|
|
|
vec![
|
|
|
|
"p".to_owned(),
|
|
|
|
"yyyyy".to_owned(),
|
|
|
|
"wss://example.com:3033".to_owned(),
|
|
|
|
],
|
|
|
|
];
|
|
|
|
let j = serde_json::to_string(&event)?;
|
|
|
|
assert_eq!(j, "{\"id\":\"0\",\"pubkey\":\"0\",\"created_at\":0,\"kind\":0,\"tags\":[[\"e\",\"xxxx\",\"wss://example.com\"],[\"p\",\"yyyyy\",\"wss://example.com:3033\"]],\"content\":\"\",\"sig\":\"0\"}");
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn event_deserialize() -> Result<()> {
|
|
|
|
let raw_json = r#"{"id":"1384757da583e6129ce831c3d7afc775a33a090578f888dd0d010328ad047d0c","pubkey":"bbbd9711d357df4f4e498841fd796535c95c8e751fa35355008a911c41265fca","created_at":1612650459,"kind":1,"tags":null,"content":"hello world","sig":"59d0cc47ab566e81f72fe5f430bcfb9b3c688cb0093d1e6daa49201c00d28ecc3651468b7938642869ed98c0f1b262998e49a05a6ed056c0d92b193f4e93bc21"}"#;
|
|
|
|
let e: Event = serde_json::from_str(raw_json)?;
|
|
|
|
assert_eq!(e.kind, 1);
|
|
|
|
assert_eq!(e.tags.len(), 0);
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn event_canonical() {
|
|
|
|
let e = Event {
|
|
|
|
id: "999".to_owned(),
|
|
|
|
pubkey: "012345".to_owned(),
|
2022-10-16 16:25:06 -04:00
|
|
|
delegated_by: None,
|
2023-01-22 10:49:49 -05:00
|
|
|
created_at: 501_234,
|
2021-12-05 17:53:26 -05:00
|
|
|
kind: 1,
|
|
|
|
tags: vec![],
|
|
|
|
content: "this is a test".to_owned(),
|
|
|
|
sig: "abcde".to_owned(),
|
2022-01-14 15:27:12 -05:00
|
|
|
tagidx: None,
|
2021-12-05 17:53:26 -05:00
|
|
|
};
|
|
|
|
let c = e.to_canonical();
|
|
|
|
let expected = Some(r#"[0,"012345",501234,1,[],"this is a test"]"#.to_owned());
|
|
|
|
assert_eq!(c, expected);
|
|
|
|
}
|
|
|
|
|
2022-02-27 12:34:10 -05:00
|
|
|
#[test]
|
|
|
|
fn event_tag_select() {
|
|
|
|
let e = Event {
|
|
|
|
id: "999".to_owned(),
|
|
|
|
pubkey: "012345".to_owned(),
|
2022-10-16 16:25:06 -04:00
|
|
|
delegated_by: None,
|
2023-01-22 10:49:49 -05:00
|
|
|
created_at: 501_234,
|
2022-02-27 12:34:10 -05:00
|
|
|
kind: 1,
|
|
|
|
tags: vec![
|
|
|
|
vec!["j".to_owned(), "abc".to_owned()],
|
|
|
|
vec!["e".to_owned(), "foo".to_owned()],
|
|
|
|
vec!["e".to_owned(), "bar".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("e");
|
|
|
|
assert_eq!(v, vec!["foo", "bar", "baz"]);
|
|
|
|
}
|
|
|
|
|
2022-10-16 16:25:06 -04:00
|
|
|
#[test]
|
|
|
|
fn event_no_tag_select() {
|
|
|
|
let e = Event {
|
|
|
|
id: "999".to_owned(),
|
|
|
|
pubkey: "012345".to_owned(),
|
|
|
|
delegated_by: None,
|
2023-01-22 10:49:49 -05:00
|
|
|
created_at: 501_234,
|
2022-10-16 16:25:06 -04:00
|
|
|
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);
|
|
|
|
}
|
|
|
|
|
2021-12-05 17:53:26 -05:00
|
|
|
#[test]
|
|
|
|
fn event_canonical_with_tags() {
|
|
|
|
let e = Event {
|
|
|
|
id: "999".to_owned(),
|
|
|
|
pubkey: "012345".to_owned(),
|
2022-10-16 16:25:06 -04:00
|
|
|
delegated_by: None,
|
2023-01-22 10:49:49 -05:00
|
|
|
created_at: 501_234,
|
2021-12-05 17:53:26 -05:00
|
|
|
kind: 1,
|
|
|
|
tags: vec![
|
|
|
|
vec!["#e".to_owned(), "aoeu".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(),
|
2022-01-14 15:27:12 -05:00
|
|
|
tagidx: None,
|
2021-12-05 17:53:26 -05:00
|
|
|
};
|
|
|
|
let c = e.to_canonical();
|
|
|
|
let expected_json = r###"[0,"012345",501234,1,[["#e","aoeu"],["#p","aaaa","ws://example.com"]],"this is a test"]"###;
|
|
|
|
let expected = Some(expected_json.to_owned());
|
|
|
|
assert_eq!(c, expected);
|
|
|
|
}
|
2023-01-15 10:18:53 -05:00
|
|
|
|
2023-01-24 09:04:42 -05:00
|
|
|
#[test]
|
|
|
|
fn ephemeral_event() {
|
|
|
|
let mut event = Event::simple_event();
|
2023-02-08 10:55:17 -05:00
|
|
|
event.kind = 20000;
|
2023-01-24 09:04:42 -05:00
|
|
|
assert!(event.is_ephemeral());
|
2023-02-08 10:55:17 -05:00
|
|
|
event.kind = 29999;
|
2023-01-24 09:04:42 -05:00
|
|
|
assert!(event.is_ephemeral());
|
2023-02-08 10:55:17 -05:00
|
|
|
event.kind = 30000;
|
2023-01-24 09:04:42 -05:00
|
|
|
assert!(!event.is_ephemeral());
|
2023-02-08 10:55:17 -05:00
|
|
|
event.kind = 19999;
|
2023-01-24 09:04:42 -05:00
|
|
|
assert!(!event.is_ephemeral());
|
|
|
|
}
|
|
|
|
|
2023-01-15 10:18:53 -05:00
|
|
|
#[test]
|
|
|
|
fn replaceable_event() {
|
2023-01-22 11:06:44 -05:00
|
|
|
let mut event = Event::simple_event();
|
2023-02-08 10:55:17 -05:00
|
|
|
event.kind = 0;
|
2023-01-22 11:06:44 -05:00
|
|
|
assert!(event.is_replaceable());
|
2023-02-08 10:55:17 -05:00
|
|
|
event.kind = 3;
|
2023-01-22 11:06:44 -05:00
|
|
|
assert!(event.is_replaceable());
|
2023-02-08 10:55:17 -05:00
|
|
|
event.kind = 10000;
|
2023-01-24 09:04:42 -05:00
|
|
|
assert!(event.is_replaceable());
|
2023-02-08 10:55:17 -05:00
|
|
|
event.kind = 19999;
|
2023-01-22 11:06:44 -05:00
|
|
|
assert!(event.is_replaceable());
|
2023-02-08 10:55:17 -05:00
|
|
|
event.kind = 20000;
|
2023-01-24 09:04:42 -05:00
|
|
|
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;
|
2023-02-08 10:55:17 -05:00
|
|
|
event.tags = vec![vec!["d".to_owned(), "".to_owned()]];
|
2023-01-24 09:04:42 -05:00
|
|
|
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()));
|
|
|
|
}
|
2023-01-15 10:18:53 -05:00
|
|
|
|
2023-01-24 09:04:42 -05:00
|
|
|
#[test]
|
|
|
|
fn param_replaceable_value_case_3() {
|
|
|
|
// NIP case #3: "tags":[["d"]]: implicit empty value ""
|
|
|
|
let mut event = Event::simple_event();
|
|
|
|
event.kind = 30000;
|
2023-02-08 10:55:17 -05:00
|
|
|
event.tags = vec![vec!["d".to_owned()]];
|
2023-01-24 09:04:42 -05:00
|
|
|
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()],
|
2023-02-08 10:55:17 -05:00
|
|
|
vec!["d".to_owned(), "not empty".to_string()],
|
2023-01-24 09:04:42 -05:00
|
|
|
];
|
|
|
|
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()],
|
2023-02-08 10:55:17 -05:00
|
|
|
vec!["d".to_owned(), "".to_string()],
|
2023-01-24 09:04:42 -05:00
|
|
|
];
|
|
|
|
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()],
|
2023-02-08 10:55:17 -05:00
|
|
|
vec!["d".to_owned(), "third value".to_string()],
|
2023-01-24 09:04:42 -05:00
|
|
|
];
|
|
|
|
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;
|
2023-02-08 10:55:17 -05:00
|
|
|
event.tags = vec![vec!["e".to_owned()]];
|
2023-01-24 09:04:42 -05:00
|
|
|
assert_eq!(event.distinct_param(), Some("".to_string()));
|
2023-01-15 10:18:53 -05:00
|
|
|
}
|
|
|
|
|
2023-02-15 19:47:27 -05:00
|
|
|
#[test]
|
|
|
|
fn expiring_event_none() {
|
|
|
|
// regular events do not expire
|
|
|
|
let mut event = Event::simple_event();
|
|
|
|
event.kind = 7;
|
2023-02-25 15:49:35 -05:00
|
|
|
event.tags = vec![vec!["test".to_string(), "foo".to_string()]];
|
2023-02-15 19:47:27 -05:00
|
|
|
assert_eq!(event.expiration(), None);
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn expiring_event_empty() {
|
|
|
|
// regular events do not expire
|
|
|
|
let mut event = Event::simple_event();
|
|
|
|
event.kind = 7;
|
2023-02-25 15:49:35 -05:00
|
|
|
event.tags = vec![vec!["expiration".to_string()]];
|
2023-02-15 19:47:27 -05:00
|
|
|
assert_eq!(event.expiration(), None);
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn expiring_event_future() {
|
|
|
|
// a normal expiring event
|
2023-02-25 15:49:35 -05:00
|
|
|
let exp: u64 = 1676264138;
|
2023-02-15 19:47:27 -05:00
|
|
|
let mut event = Event::simple_event();
|
|
|
|
event.kind = 1;
|
2023-02-25 15:49:35 -05:00
|
|
|
event.tags = vec![vec!["expiration".to_string(), exp.to_string()]];
|
2023-02-15 19:47:27 -05:00
|
|
|
assert_eq!(event.expiration(), Some(exp));
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn expiring_event_negative() {
|
|
|
|
// expiration set to a negative value (invalid)
|
2023-02-25 15:49:35 -05:00
|
|
|
let exp: i64 = -90;
|
2023-02-15 19:47:27 -05:00
|
|
|
let mut event = Event::simple_event();
|
|
|
|
event.kind = 1;
|
2023-02-25 15:49:35 -05:00
|
|
|
event.tags = vec![vec!["expiration".to_string(), exp.to_string()]];
|
2023-02-15 19:47:27 -05:00
|
|
|
assert_eq!(event.expiration(), None);
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn expiring_event_zero() {
|
|
|
|
// a normal expiring event set to zero
|
2023-02-25 15:49:35 -05:00
|
|
|
let exp: i64 = 0;
|
2023-02-15 19:47:27 -05:00
|
|
|
let mut event = Event::simple_event();
|
|
|
|
event.kind = 1;
|
2023-02-25 15:49:35 -05:00
|
|
|
event.tags = vec![vec!["expiration".to_string(), exp.to_string()]];
|
2023-02-15 19:47:27 -05:00
|
|
|
assert_eq!(event.expiration(), Some(0));
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn expiring_event_fraction() {
|
|
|
|
// expiration is fractional (invalid)
|
2023-02-25 15:49:35 -05:00
|
|
|
let exp: f64 = 23.334;
|
2023-02-15 19:47:27 -05:00
|
|
|
let mut event = Event::simple_event();
|
|
|
|
event.kind = 1;
|
2023-02-25 15:49:35 -05:00
|
|
|
event.tags = vec![vec!["expiration".to_string(), exp.to_string()]];
|
2023-02-15 19:47:27 -05:00
|
|
|
assert_eq!(event.expiration(), None);
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn expiring_event_multiple() {
|
|
|
|
// multiple values, we just take the first
|
|
|
|
let mut event = Event::simple_event();
|
|
|
|
event.kind = 1;
|
|
|
|
event.tags = vec![
|
|
|
|
vec!["expiration".to_string(), (10).to_string()],
|
|
|
|
vec!["expiration".to_string(), (20).to_string()],
|
|
|
|
];
|
|
|
|
assert_eq!(event.expiration(), Some(10));
|
|
|
|
}
|
2021-12-05 17:53:26 -05:00
|
|
|
}
|