Better mute handling

This commit improves the robustness of mute list handling:
1. It listens to new mute lists from the relay interface, and saves them whenever there is a new one
2. It uses the user's own relay lists to fetch events (such as mute lists), helping to ensure we get their mute lists even when they are not stored in the Damus relay
3. It saves events it sees when fetching them from the network, if applicable

Changelog-Changed: Improved robustness of mute handling
Signed-off-by: Daniel D’Aquino <daniel@daquino.me>
This commit is contained in:
Daniel D’Aquino
2024-12-02 19:19:29 +09:00
parent f6bbc9e1fe
commit 88939ba2d1
9 changed files with 428 additions and 101 deletions

View File

@ -1,5 +1,5 @@
use nostr::{self, key::PublicKey, nips::nip51::MuteList, Alphabet, SingleLetterTag, TagKind::SingleLetter};
use nostr_sdk::{Kind, TagKind};
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 {
@ -14,7 +14,7 @@ pub trait ExtendedEvent {
/// 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>;
}
@ -48,7 +48,7 @@ impl ExtendedEvent for nostr::Event {
.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)))
@ -102,12 +102,16 @@ 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 {
Some(MuteList {
public_keys: self.referenced_pubkeys().iter().map(|pk| pk.clone()).collect(),
hashtags: self.referenced_hashtags().iter().map(|tag| tag.clone()).collect(),
event_ids: self.referenced_event_ids().iter().map(|id| id.clone()).collect(),
@ -115,3 +119,104 @@ impl MaybeConvertibleToMuteList for nostr::Event {
})
}
}
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.clone(),
})
}
}
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.as_ref().map(|m| m.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,
})
}
}
pub struct TimestampedMuteList {
pub mute_list: MuteList,
pub timestamp: nostr::Timestamp,
}