238 lines
7.2 KiB
Rust
238 lines
7.2 KiB
Rust
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<Pubkey>,
|
|
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<Self, ParseError<'a>> {
|
|
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<Pubkey> = 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::<bech32::Bech32>(hrp, &buf).expect("expected bech32 nfilter to encode ok")
|
|
}
|
|
|
|
fn decode_value(value: &Value) -> Option<Self> {
|
|
let mut search: Option<String> = None;
|
|
let mut author: Option<Pubkey> = 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<Self> {
|
|
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());
|
|
}
|
|
}
|