notedeck

One damus client to rule them all
git clone git://jb55.com/notedeck
Log | Files | Refs | README | LICENSE

search.rs (7396B)


      1 use enostr::Pubkey;
      2 use nostrdb::{Filter, FilterBuilder};
      3 use rmpv::Value;
      4 use tokenator::{ParseError, TokenParser, TokenSerializable, TokenWriter};
      5 
      6 #[derive(Debug, Eq, PartialEq, Clone, Hash)]
      7 pub struct SearchQuery {
      8     author: Option<Pubkey>,
      9     pub search: String,
     10 }
     11 
     12 impl TokenSerializable for SearchQuery {
     13     fn serialize_tokens(&self, writer: &mut TokenWriter) {
     14         writer.write_token(&self.to_nfilter())
     15     }
     16 
     17     fn parse_from_tokens<'a>(parser: &mut TokenParser<'a>) -> Result<Self, ParseError<'a>> {
     18         if let Some(query) = SearchQuery::from_nfilter(parser.pull_token()?) {
     19             Ok(query)
     20         } else {
     21             Err(ParseError::DecodeFailed)
     22         }
     23     }
     24 }
     25 
     26 impl SearchQuery {
     27     pub fn new(search: String) -> Self {
     28         let author: Option<Pubkey> = None;
     29         Self { search, author }
     30     }
     31     /// Convert the query to a filter-compatible MessagePack value
     32     fn to_msgpack_value(&self) -> Value {
     33         let mut values: Vec<(Value, Value)> = Vec::with_capacity(2);
     34         let search_str: &str = &self.search;
     35         values.push(("search".into(), search_str.into()));
     36         if let Some(pubkey) = self.author() {
     37             values.push((
     38                 "authors".into(),
     39                 Value::Array(vec![Value::Binary(pubkey.bytes().to_vec())]),
     40             ))
     41         }
     42 
     43         Value::Map(values)
     44     }
     45 
     46     pub fn to_nfilter(&self) -> String {
     47         let hrp = bech32::Hrp::parse_unchecked("nfilter");
     48         let msgpack_value = self.to_msgpack_value();
     49         let mut buf = vec![];
     50         rmpv::encode::write_value(&mut buf, &msgpack_value)
     51             .expect("expected nfilter to encode ok. too big?");
     52 
     53         bech32::encode::<bech32::Bech32>(hrp, &buf).expect("expected bech32 nfilter to encode ok")
     54     }
     55 
     56     fn decode_value(value: &Value) -> Option<Self> {
     57         let mut search: Option<String> = None;
     58         let mut author: Option<Pubkey> = None;
     59 
     60         let values = if let Value::Map(values) = value {
     61             values
     62         } else {
     63             return None;
     64         };
     65 
     66         for (key, value) in values {
     67             let key_str: &str = if let Value::String(s) = key {
     68                 s.as_str()?
     69             } else {
     70                 continue;
     71             };
     72 
     73             if key_str == "search" {
     74                 if let Value::String(search_str) = value {
     75                     search = search_str.clone().into_str();
     76                 } else {
     77                     continue;
     78                 }
     79             } else if key_str == "authors" {
     80                 let authors = if let Value::Array(authors) = value {
     81                     authors
     82                 } else {
     83                     continue;
     84                 };
     85 
     86                 let author_value = if let Some(author_value) = authors.first() {
     87                     author_value
     88                 } else {
     89                     continue;
     90                 };
     91 
     92                 let author_bytes: &[u8] = if let Value::Binary(author_bytes) = author_value {
     93                     author_bytes
     94                 } else {
     95                     continue;
     96                 };
     97 
     98                 let pubkey = Pubkey::new(author_bytes.try_into().ok()?);
     99                 author = Some(pubkey);
    100             }
    101         }
    102 
    103         let search = search?;
    104 
    105         Some(Self { search, author })
    106     }
    107 
    108     pub fn filter(&self) -> FilterBuilder {
    109         Filter::new().search(&self.search).kinds([1])
    110     }
    111 
    112     pub fn from_nfilter(nfilter: &str) -> Option<Self> {
    113         let (hrp, msgpack_data) = bech32::decode(nfilter).ok()?;
    114         if hrp.as_str() != "nfilter" {
    115             return None;
    116         }
    117 
    118         let value = rmpv::decode::read_value(&mut &msgpack_data[..]).ok()?;
    119 
    120         Self::decode_value(&value)
    121     }
    122 
    123     pub fn author(&self) -> Option<&Pubkey> {
    124         self.author.as_ref()
    125     }
    126 }
    127 
    128 #[cfg(test)]
    129 mod tests {
    130     use super::*;
    131     use enostr::Pubkey;
    132     use rmpv::Value;
    133     use tokenator::{TokenParser, TokenSerializable, TokenWriter};
    134 
    135     fn test_pubkey() -> Pubkey {
    136         let bytes: [u8; 32] = [1; 32]; // Example public key
    137         Pubkey::new(bytes)
    138     }
    139 
    140     #[test]
    141     fn test_to_msgpack_value() {
    142         let query = SearchQuery {
    143             author: Some(test_pubkey()),
    144             search: "nostrdb".to_string(),
    145         };
    146         let msgpack_value = query.to_msgpack_value();
    147 
    148         if let Value::Map(values) = msgpack_value {
    149             assert!(values
    150                 .iter()
    151                 .any(|(k, v)| *k == Value::String("search".into())
    152                     && *v == Value::String("nostrdb".into())));
    153             assert!(values
    154                 .iter()
    155                 .any(|(k, _v)| *k == Value::String("authors".into())));
    156         } else {
    157             panic!("Failed to encode SearchQuery to MessagePack");
    158         }
    159     }
    160 
    161     #[test]
    162     fn test_to_nfilter() {
    163         let query = SearchQuery {
    164             author: Some(test_pubkey()),
    165             search: "nostrdb".to_string(),
    166         };
    167         let encoded = query.to_nfilter();
    168         assert!(encoded.starts_with("nfilter"), "nfilter encoding failed");
    169     }
    170 
    171     #[test]
    172     fn test_from_nfilter() {
    173         let query = SearchQuery {
    174             author: Some(test_pubkey()),
    175             search: "nostrdb".to_string(),
    176         };
    177         let encoded = query.to_nfilter();
    178         let decoded = SearchQuery::from_nfilter(&encoded).expect("Failed to decode nfilter");
    179         assert_eq!(query, decoded);
    180     }
    181 
    182     #[test]
    183     fn test_nfilter_roundtrip() {
    184         let queries = vec![
    185             SearchQuery {
    186                 author: None,
    187                 search: "nostrdb".to_string(),
    188             },
    189             SearchQuery {
    190                 author: Some(test_pubkey()),
    191                 search: "test".to_string(),
    192             },
    193         ];
    194 
    195         for query in queries {
    196             let encoded = query.to_nfilter();
    197             let decoded =
    198                 SearchQuery::from_nfilter(&encoded).expect("Failed to decode valid nfilter");
    199             assert_eq!(query, decoded, "Roundtrip encoding/decoding failed");
    200         }
    201     }
    202 
    203     #[test]
    204     fn test_invalid_nfilter() {
    205         let invalid_nfilter = "nfilter1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq";
    206         assert!(SearchQuery::from_nfilter(invalid_nfilter).is_none());
    207     }
    208 
    209     #[test]
    210     fn test_invalid_hrp() {
    211         let invalid_nfilter = "invalid1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq";
    212         assert!(SearchQuery::from_nfilter(invalid_nfilter).is_none());
    213     }
    214 
    215     #[test]
    216     fn test_parse_from_tokens() {
    217         let query = SearchQuery {
    218             author: Some(test_pubkey()),
    219             search: "nostrdb".to_string(),
    220         };
    221         let mut writer = TokenWriter::default();
    222         query.serialize_tokens(&mut writer);
    223         let tokens = [writer.str()];
    224         let mut parser = TokenParser::new(&tokens);
    225 
    226         let parsed =
    227             SearchQuery::parse_from_tokens(&mut parser).expect("Failed to parse from tokens");
    228 
    229         assert_eq!(query, parsed);
    230     }
    231 
    232     #[test]
    233     fn test_parse_from_invalid_tokens() {
    234         let mut parser = TokenParser::new(&[]);
    235         assert!(SearchQuery::parse_from_tokens(&mut parser).is_err());
    236     }
    237 }