mirror of
https://github.com/nostrlabs-io/notepush.git
synced 2025-06-16 11:48:51 +00:00
273 lines
11 KiB
Rust
273 lines
11 KiB
Rust
use nostr::{
|
|
self,
|
|
key::PublicKey,
|
|
nips::{nip51::MuteList, nip65},
|
|
Alphabet, SingleLetterTag,
|
|
TagKind::SingleLetter,
|
|
};
|
|
use nostr_sdk::{EventId, Kind, TagKind};
|
|
|
|
/// Temporary scaffolding of old methods that have not been ported to use native Event methods
|
|
pub trait ExtendedEvent {
|
|
/// Retrieves a set of pubkeys referenced by the note
|
|
fn referenced_pubkeys(&self) -> std::collections::HashSet<nostr::PublicKey>;
|
|
|
|
/// Retrieves a set of pubkeys relevant to the note
|
|
fn relevant_pubkeys(&self) -> std::collections::HashSet<nostr::PublicKey>;
|
|
|
|
/// Retrieves a set of event IDs referenced by the note
|
|
fn referenced_event_ids(&self) -> std::collections::HashSet<nostr::EventId>;
|
|
|
|
/// Retrieves a set of hashtags (t tags) referenced by the note
|
|
fn referenced_hashtags(&self) -> std::collections::HashSet<String>;
|
|
}
|
|
|
|
// This is a wrapper around the Event type from strfry-policies, which adds some useful methods
|
|
impl ExtendedEvent for nostr::Event {
|
|
/// Retrieves a set of pubkeys referenced by the note
|
|
fn referenced_pubkeys(&self) -> std::collections::HashSet<nostr::PublicKey> {
|
|
self.get_tags_content(SingleLetter(SingleLetterTag::lowercase(Alphabet::P)))
|
|
.iter()
|
|
.filter_map(|tag| PublicKey::from_hex(tag).ok())
|
|
.collect()
|
|
}
|
|
|
|
/// Retrieves a set of pubkeys relevant to the note
|
|
fn relevant_pubkeys(&self) -> std::collections::HashSet<nostr::PublicKey> {
|
|
let mut pubkeys = self.referenced_pubkeys();
|
|
pubkeys.insert(self.pubkey);
|
|
pubkeys
|
|
}
|
|
|
|
/// Retrieves a set of event IDs referenced by the note
|
|
fn referenced_event_ids(&self) -> std::collections::HashSet<nostr::EventId> {
|
|
self.get_tag_content(SingleLetter(SingleLetterTag::lowercase(Alphabet::E)))
|
|
.iter()
|
|
.filter_map(|tag| nostr::EventId::from_hex(tag).ok())
|
|
.collect()
|
|
}
|
|
|
|
/// Retrieves a set of hashtags (t tags) referenced by the note
|
|
fn referenced_hashtags(&self) -> std::collections::HashSet<String> {
|
|
self.get_tags_content(SingleLetter(SingleLetterTag::lowercase(Alphabet::T)))
|
|
.iter()
|
|
.map(|tag| tag.to_string())
|
|
.collect()
|
|
}
|
|
}
|
|
|
|
// MARK: - SQL String Convertible
|
|
|
|
pub trait SqlStringConvertible {
|
|
fn to_sql_string(&self) -> String;
|
|
fn from_sql_string(s: String) -> Result<Self, Box<dyn std::error::Error>>
|
|
where
|
|
Self: Sized;
|
|
}
|
|
|
|
impl SqlStringConvertible for nostr::EventId {
|
|
fn to_sql_string(&self) -> String {
|
|
self.to_hex()
|
|
}
|
|
|
|
fn from_sql_string(s: String) -> Result<Self, Box<dyn std::error::Error>> {
|
|
nostr::EventId::from_hex(s).map_err(|e| e.into())
|
|
}
|
|
}
|
|
|
|
impl SqlStringConvertible for nostr::PublicKey {
|
|
fn to_sql_string(&self) -> String {
|
|
self.to_hex()
|
|
}
|
|
|
|
fn from_sql_string(s: String) -> Result<Self, Box<dyn std::error::Error>> {
|
|
nostr::PublicKey::from_hex(s).map_err(|e| e.into())
|
|
}
|
|
}
|
|
|
|
impl SqlStringConvertible for nostr::Timestamp {
|
|
fn to_sql_string(&self) -> String {
|
|
self.as_u64().to_string()
|
|
}
|
|
|
|
fn from_sql_string(s: String) -> Result<Self, Box<dyn std::error::Error>> {
|
|
let u64_timestamp: u64 = s.parse()?;
|
|
Ok(nostr::Timestamp::from(u64_timestamp))
|
|
}
|
|
}
|
|
|
|
pub trait MaybeConvertibleToMuteList {
|
|
fn to_mute_list(&self) -> Option<MuteList>;
|
|
}
|
|
|
|
pub trait MaybeConvertibleToTimestampedMuteList {
|
|
fn to_timestamped_mute_list(&self) -> Option<TimestampedMuteList>;
|
|
}
|
|
|
|
impl MaybeConvertibleToMuteList for nostr::Event {
|
|
fn to_mute_list(&self) -> Option<MuteList> {
|
|
if self.kind != Kind::MuteList {
|
|
return None;
|
|
}
|
|
Some(MuteList {
|
|
public_keys: self.referenced_pubkeys().iter().copied().collect(),
|
|
hashtags: self.referenced_hashtags().iter().cloned().collect(),
|
|
event_ids: self.referenced_event_ids().iter().copied().collect(),
|
|
words: self
|
|
.get_tags_content(TagKind::Word)
|
|
.iter()
|
|
.map(|tag| tag.to_string())
|
|
.collect(),
|
|
})
|
|
}
|
|
}
|
|
|
|
impl MaybeConvertibleToTimestampedMuteList for nostr::Event {
|
|
fn to_timestamped_mute_list(&self) -> Option<TimestampedMuteList> {
|
|
if self.kind != Kind::MuteList {
|
|
return None;
|
|
}
|
|
let mute_list = self.to_mute_list()?;
|
|
Some(TimestampedMuteList {
|
|
mute_list,
|
|
timestamp: self.created_at,
|
|
})
|
|
}
|
|
}
|
|
|
|
pub type RelayList = Vec<(nostr::Url, Option<nostr::nips::nip65::RelayMetadata>)>;
|
|
|
|
pub trait MaybeConvertibleToRelayList {
|
|
fn to_relay_list(&self) -> Option<RelayList>;
|
|
}
|
|
|
|
impl MaybeConvertibleToRelayList for nostr::Event {
|
|
fn to_relay_list(&self) -> Option<RelayList> {
|
|
if self.kind != Kind::RelayList {
|
|
return None;
|
|
}
|
|
let extracted_relay_list = nip65::extract_relay_list(self);
|
|
// Convert the extracted relay list data fully into owned data that can be returned
|
|
let extracted_relay_list_owned = extracted_relay_list
|
|
.into_iter()
|
|
.map(|(url, metadata)| (url.clone(), metadata.clone()))
|
|
.collect();
|
|
|
|
Some(extracted_relay_list_owned)
|
|
}
|
|
}
|
|
|
|
/// A trait for types that can be encoded to and decoded from JSON, specific to this crate.
|
|
/// This is defined to overcome the rust compiler's limitation of implementing a trait for a type that is not defined in the same crate.
|
|
pub trait Codable {
|
|
fn to_json(&self) -> Result<serde_json::Value, Box<dyn std::error::Error>>;
|
|
fn from_json(json: serde_json::Value) -> Result<Self, Box<dyn std::error::Error>>
|
|
where
|
|
Self: Sized;
|
|
}
|
|
|
|
impl Codable for MuteList {
|
|
fn to_json(&self) -> Result<serde_json::Value, Box<dyn std::error::Error>> {
|
|
Ok(serde_json::json!({
|
|
"public_keys": self.public_keys.iter().map(|pk| pk.to_hex()).collect::<Vec<String>>(),
|
|
"hashtags": self.hashtags.clone(),
|
|
"event_ids": self.event_ids.iter().map(|id| id.to_hex()).collect::<Vec<String>>(),
|
|
"words": self.words.clone()
|
|
}))
|
|
}
|
|
|
|
fn from_json(json: serde_json::Value) -> Result<Self, Box<dyn std::error::Error>>
|
|
where
|
|
Self: Sized,
|
|
{
|
|
let public_keys = json
|
|
.get("public_keys")
|
|
.ok_or_else(|| "Missing 'public_keys' field".to_string())?
|
|
.as_array()
|
|
.ok_or_else(|| "'public_keys' must be an array".to_string())?
|
|
.iter()
|
|
.map(|pk| {
|
|
PublicKey::from_hex(pk.as_str().unwrap_or_default()).map_err(|e| e.to_string())
|
|
})
|
|
.collect::<Result<Vec<PublicKey>, String>>()?;
|
|
|
|
let hashtags = json
|
|
.get("hashtags")
|
|
.ok_or_else(|| "Missing 'hashtags' field".to_string())?
|
|
.as_array()
|
|
.ok_or_else(|| "'hashtags' must be an array".to_string())?
|
|
.iter()
|
|
.map(|tag| {
|
|
tag.as_str()
|
|
.map(|s| s.to_string())
|
|
.ok_or_else(|| "Invalid hashtag".to_string())
|
|
})
|
|
.collect::<Result<Vec<String>, String>>()?;
|
|
|
|
let event_ids = json
|
|
.get("event_ids")
|
|
.ok_or_else(|| "Missing 'event_ids' field".to_string())?
|
|
.as_array()
|
|
.ok_or_else(|| "'event_ids' must be an array".to_string())?
|
|
.iter()
|
|
.map(|id| EventId::from_hex(id.as_str().unwrap_or_default()).map_err(|e| e.to_string()))
|
|
.collect::<Result<Vec<EventId>, String>>()?;
|
|
|
|
let words = json
|
|
.get("words")
|
|
.ok_or_else(|| "Missing 'words' field".to_string())?
|
|
.as_array()
|
|
.ok_or_else(|| "'words' must be an array".to_string())?
|
|
.iter()
|
|
.map(|word| {
|
|
word.as_str()
|
|
.map(|s| s.to_string())
|
|
.ok_or_else(|| "Invalid word".to_string())
|
|
})
|
|
.collect::<Result<Vec<String>, String>>()?;
|
|
|
|
Ok(MuteList {
|
|
public_keys,
|
|
hashtags,
|
|
event_ids,
|
|
words,
|
|
})
|
|
}
|
|
}
|
|
|
|
#[derive(Clone)]
|
|
pub struct TimestampedMuteList {
|
|
pub mute_list: MuteList,
|
|
pub timestamp: nostr::Timestamp,
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use nostr::{Event, PublicKey};
|
|
|
|
#[test]
|
|
fn test_relevant_pubkeys() {
|
|
// Define the event data based on the provided JSON
|
|
let event_data = r#"{"kind":9735,"tags":[["p","4d4fb5ff0afb8c04e6c6e03f51281b664576f985e5bc34a3a7ee310a1e821f47"],["e","7ef9165e1d68424b5e34134ecaa47411863f736f55a0c08f3a00db517fa15507"],["bolt11","lnbc19710n1pnwg0kdpp57003xutqz8pp9yhjwju243gdgelskndj2prt7gfhkdvmskp24r8qhp5ulu3sphjfgt8tdasqsptaz5xuxcvtmq7fhzdnmk5y3rpzwv6huwqcqzzsxqrrs0sp549znyngm55n9gpsexy0d92zvcqasqe5d0k7rdg4s66u2sez7fdhq9qyyssqpwd7ar8yez6k2yymn07z3ejkfxjzw4rld80jgq740hsszwk5m4nnh2hvx74nqs5cvwysafjlu5uu6p9t9heuqk6tdjkz3fpmj32raxsppy7ync"],["description","{\"id\":\"cdf701cd336d74ae2a234b9b3490a8e641575f1995b1a874dc32a42666454d11\",\"pubkey\":\"32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245\",\"created_at\":1726234316,\"kind\":9734,\"tags\":[[\"e\",\"7ef9165e1d68424b5e34134ecaa47411863f736f55a0c08f3a00db517fa15507\"],[\"p\",\"4d4fb5ff0afb8c04e6c6e03f51281b664576f985e5bc34a3a7ee310a1e821f47\"],[\"relays\",\"wss:\/\/nos.lol\",\"wss:\/\/theforest.nostr1.com\",\"wss:\/\/relay.damus.io\",\"wss:\/\/nostr.wine\",\"ws:\/\/monad.jb55.com:8080\",\"wss:\/\/relay.mostr.pub\"]],\"content\":\"\",\"sig\":\"859afb22644588574fa7002dac79a9fb6a4eb2715d494614578854420b9c3e6f7a3034baf2f6bc99b2baecbfcf22dad32cfd44dfa5b42ddd0e0c6198635aa0c1\"}"],["P","32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245"]],"created_at":1726234322,"content":"","sig":"3c9bb7553278e444addc591ee3565657a8b783d47a4438cadbed6ed1ae448d470e4d2471acd21a7ab26d7a2af8c102538f00e21bdcaabf213a5fceed2895e27a","id":"6901102ac61ecfb3a051acdd2103f0b4ea7cbe4dcc58bee47ce7d8b621cd5a7b","pubkey":"f81611363554b64306467234d7396ec88455707633f54738f6c4683535098cd3"}"#;
|
|
|
|
// Parse the event from JSON
|
|
let event: Event =
|
|
serde_json::from_str(event_data).expect("Event data should be valid JSON");
|
|
|
|
// Obtain relevant pubkeys
|
|
let relevant_pubkeys = event.relevant_pubkeys();
|
|
|
|
// Expected pubkey
|
|
let expected_pubkey =
|
|
PublicKey::from_hex("4d4fb5ff0afb8c04e6c6e03f51281b664576f985e5bc34a3a7ee310a1e821f47")
|
|
.expect("Should be valid hex");
|
|
let unexpected_pubkey =
|
|
PublicKey::from_hex("32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245")
|
|
.expect("Should be valid hex");
|
|
|
|
assert!(!relevant_pubkeys.contains(&unexpected_pubkey));
|
|
assert!(relevant_pubkeys.contains(&expected_pubkey));
|
|
}
|
|
}
|