use enostr::Pubkey; use nostrdb::{Filter, FilterBuilder}; use rmpv::Value; use tokenator::{ParseError, TokenParser, TokenSerializable, TokenWriter}; #[derive(Debug, Eq, PartialEq, Clone, Hash)] pub struct SearchQuery { author: Option, pub search: String, } impl TokenSerializable for SearchQuery { fn serialize_tokens(&self, writer: &mut TokenWriter) { writer.write_token(&self.to_nfilter()) } fn parse_from_tokens<'a>(parser: &mut TokenParser<'a>) -> Result> { if let Some(query) = SearchQuery::from_nfilter(parser.pull_token()?) { Ok(query) } else { Err(ParseError::DecodeFailed) } } } impl SearchQuery { pub fn new(search: String) -> Self { let author: Option = None; Self { search, author } } /// Convert the query to a filter-compatible MessagePack value fn to_msgpack_value(&self) -> Value { let mut values: Vec<(Value, Value)> = Vec::with_capacity(2); let search_str: &str = &self.search; values.push(("search".into(), search_str.into())); if let Some(pubkey) = self.author() { values.push(( "authors".into(), Value::Array(vec![Value::Binary(pubkey.bytes().to_vec())]), )) } Value::Map(values) } pub fn to_nfilter(&self) -> String { let hrp = bech32::Hrp::parse_unchecked("nfilter"); let msgpack_value = self.to_msgpack_value(); let mut buf = vec![]; rmpv::encode::write_value(&mut buf, &msgpack_value) .expect("expected nfilter to encode ok. too big?"); bech32::encode::(hrp, &buf).expect("expected bech32 nfilter to encode ok") } fn decode_value(value: &Value) -> Option { let mut search: Option = None; let mut author: Option = None; let values = if let Value::Map(values) = value { values } else { return None; }; for (key, value) in values { let key_str: &str = if let Value::String(s) = key { s.as_str()? } else { continue; }; if key_str == "search" { if let Value::String(search_str) = value { search = search_str.clone().into_str(); } else { continue; } } else if key_str == "authors" { let authors = if let Value::Array(authors) = value { authors } else { continue; }; let author_value = if let Some(author_value) = authors.first() { author_value } else { continue; }; let author_bytes: &[u8] = if let Value::Binary(author_bytes) = author_value { author_bytes } else { continue; }; let pubkey = Pubkey::new(author_bytes.try_into().ok()?); author = Some(pubkey); } } let search = search?; Some(Self { search, author }) } pub fn filter(&self) -> FilterBuilder { Filter::new().search(&self.search).kinds([1]) } pub fn from_nfilter(nfilter: &str) -> Option { let (hrp, msgpack_data) = bech32::decode(nfilter).ok()?; if hrp.as_str() != "nfilter" { return None; } let value = rmpv::decode::read_value(&mut &msgpack_data[..]).ok()?; Self::decode_value(&value) } pub fn author(&self) -> Option<&Pubkey> { self.author.as_ref() } } #[cfg(test)] mod tests { use super::*; use enostr::Pubkey; use rmpv::Value; use tokenator::{TokenParser, TokenSerializable, TokenWriter}; fn test_pubkey() -> Pubkey { let bytes: [u8; 32] = [1; 32]; // Example public key Pubkey::new(bytes) } #[test] fn test_to_msgpack_value() { let query = SearchQuery { author: Some(test_pubkey()), search: "nostrdb".to_string(), }; let msgpack_value = query.to_msgpack_value(); if let Value::Map(values) = msgpack_value { assert!(values .iter() .any(|(k, v)| *k == Value::String("search".into()) && *v == Value::String("nostrdb".into()))); assert!(values .iter() .any(|(k, _v)| *k == Value::String("authors".into()))); } else { panic!("Failed to encode SearchQuery to MessagePack"); } } #[test] fn test_to_nfilter() { let query = SearchQuery { author: Some(test_pubkey()), search: "nostrdb".to_string(), }; let encoded = query.to_nfilter(); assert!(encoded.starts_with("nfilter"), "nfilter encoding failed"); } #[test] fn test_from_nfilter() { let query = SearchQuery { author: Some(test_pubkey()), search: "nostrdb".to_string(), }; let encoded = query.to_nfilter(); let decoded = SearchQuery::from_nfilter(&encoded).expect("Failed to decode nfilter"); assert_eq!(query, decoded); } #[test] fn test_nfilter_roundtrip() { let queries = vec![ SearchQuery { author: None, search: "nostrdb".to_string(), }, SearchQuery { author: Some(test_pubkey()), search: "test".to_string(), }, ]; for query in queries { let encoded = query.to_nfilter(); let decoded = SearchQuery::from_nfilter(&encoded).expect("Failed to decode valid nfilter"); assert_eq!(query, decoded, "Roundtrip encoding/decoding failed"); } } #[test] fn test_invalid_nfilter() { let invalid_nfilter = "nfilter1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq"; assert!(SearchQuery::from_nfilter(invalid_nfilter).is_none()); } #[test] fn test_invalid_hrp() { let invalid_nfilter = "invalid1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq"; assert!(SearchQuery::from_nfilter(invalid_nfilter).is_none()); } #[test] fn test_parse_from_tokens() { let query = SearchQuery { author: Some(test_pubkey()), search: "nostrdb".to_string(), }; let mut writer = TokenWriter::default(); query.serialize_tokens(&mut writer); let tokens = [writer.str()]; let mut parser = TokenParser::new(&tokens); let parsed = SearchQuery::parse_from_tokens(&mut parser).expect("Failed to parse from tokens"); assert_eq!(query, parsed); } #[test] fn test_parse_from_invalid_tokens() { let mut parser = TokenParser::new(&[]); assert!(SearchQuery::parse_from_tokens(&mut parser).is_err()); } }